One Hat Cyber Team
Your IP:
216.73.216.102
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
home
/
fluxyjvi
/
public_html
/
assets
/
images
/
View File Name :
Monolog.tar
ResettableInterface.php 0000644 00000001716 15107322011 0011165 0 ustar 00 <?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 or Processor implementing this interface will be reset when Logger::reset() is called. * * Resetting ends a log cycle gets them back to their initial state. * * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal * state, and getting it back to a state in which it can receive log records again. * * This is useful in case you want to avoid logs leaking between two requests or jobs when you * have a long running process like a worker or an application server serving multiple requests * in one process. * * @author Grégoire Pineau <lyrixx@lyrixx.info> */ interface ResettableInterface { public function reset(): void; } SignalHandler.php 0000644 00000007466 15107322011 0007775 0 ustar 00 <?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; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use ReflectionExtension; /** * Monolog POSIX signal handler * * @author Robert Gust-Bardon <robert@gust-bardon.org> */ class SignalHandler { private LoggerInterface $logger; /** @var array<int, callable|string|int> SIG_DFL, SIG_IGN or previous callable */ private array $previousSignalHandler = []; /** @var array<int, \Psr\Log\LogLevel::*> */ private array $signalLevelMap = []; /** @var array<int, bool> */ private array $signalRestartSyscalls = []; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } /** * @param int|string|Level $level Level or level name * @return $this * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function registerSignalHandler(int $signo, int|string|Level $level = LogLevel::CRITICAL, bool $callPrevious = true, bool $restartSyscalls = true, ?bool $async = true): self { if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { return $this; } $level = Logger::toMonologLevel($level)->toPsrLogLevel(); if ($callPrevious) { $handler = pcntl_signal_get_handler($signo); $this->previousSignalHandler[$signo] = $handler; } else { unset($this->previousSignalHandler[$signo]); } $this->signalLevelMap[$signo] = $level; $this->signalRestartSyscalls[$signo] = $restartSyscalls; if ($async !== null) { pcntl_async_signals($async); } pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); return $this; } /** * @param mixed $siginfo */ public function handleSignal(int $signo, $siginfo = null): void { /** @var array<int, string> $signals */ static $signals = []; if (\count($signals) === 0 && extension_loaded('pcntl')) { $pcntl = new ReflectionExtension('pcntl'); foreach ($pcntl->getConstants() as $name => $value) { if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { $signals[$value] = $name; } } } $level = $this->signalLevelMap[$signo] ?? LogLevel::CRITICAL; $signal = $signals[$signo] ?? $signo; $context = $siginfo ?? []; $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); if (!isset($this->previousSignalHandler[$signo])) { return; } if ($this->previousSignalHandler[$signo] === SIG_DFL) { if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill') ) { $restartSyscalls = $this->signalRestartSyscalls[$signo] ?? true; pcntl_signal($signo, SIG_DFL, $restartSyscalls); pcntl_sigprocmask(SIG_UNBLOCK, [$signo], $oldset); posix_kill(posix_getpid(), $signo); pcntl_signal_dispatch(); pcntl_sigprocmask(SIG_SETMASK, $oldset); pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); } } elseif (is_callable($this->previousSignalHandler[$signo])) { $this->previousSignalHandler[$signo]($signo, $siginfo); } } } Utils.php 0000644 00000022256 15107322011 0006354 0 ustar 00 <?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; final class Utils { const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR; public static function getClass(object $object): string { $class = \get_class($object); if (false === ($pos = \strpos($class, "@anonymous\0"))) { return $class; } if (false === ($parent = \get_parent_class($class))) { return \substr($class, 0, $pos + 10); } return $parent . '@anonymous'; } public static function substr(string $string, int $start, ?int $length = null): string { if (extension_loaded('mbstring')) { return mb_strcut($string, $start, $length); } return substr($string, $start, (null === $length) ? strlen($string) : $length); } /** * Makes sure if a relative path is passed in it is turned into an absolute path * * @param string $streamUrl stream URL or path without protocol */ public static function canonicalizePath(string $streamUrl): string { $prefix = ''; if ('file://' === substr($streamUrl, 0, 7)) { $streamUrl = substr($streamUrl, 7); $prefix = 'file://'; } // other type of stream, not supported if (false !== strpos($streamUrl, '://')) { return $streamUrl; } // already absolute if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') { return $prefix.$streamUrl; } $streamUrl = getcwd() . '/' . $streamUrl; return $prefix.$streamUrl; } /** * Return the JSON representation of a value * * @param mixed $data * @param int $encodeFlags flags to pass to json encode, defaults to DEFAULT_JSON_FLAGS * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null * @throws \RuntimeException if encoding fails and errors are not ignored * @return string when errors are ignored and the encoding fails, "null" is returned which is valid json for null */ public static function jsonEncode($data, ?int $encodeFlags = null, bool $ignoreErrors = false): string { if (null === $encodeFlags) { $encodeFlags = self::DEFAULT_JSON_FLAGS; } if ($ignoreErrors) { $json = @json_encode($data, $encodeFlags); if (false === $json) { return 'null'; } return $json; } $json = json_encode($data, $encodeFlags); if (false === $json) { $json = self::handleJsonError(json_last_error(), $data); } return $json; } /** * Handle a json_encode failure. * * If the failure is due to invalid string encoding, try to clean the * input and encode again. If the second encoding attempt fails, the * initial error is not encoding related or the input can't be cleaned then * raise a descriptive exception. * * @param int $code return code of json_last_error function * @param mixed $data data that was meant to be encoded * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION * @throws \RuntimeException if failure can't be corrected * @return string JSON encoded data after error correction */ public static function handleJsonError(int $code, $data, ?int $encodeFlags = null): string { if ($code !== JSON_ERROR_UTF8) { self::throwEncodeError($code, $data); } if (is_string($data)) { self::detectAndCleanUtf8($data); } elseif (is_array($data)) { array_walk_recursive($data, ['Monolog\Utils', 'detectAndCleanUtf8']); } else { self::throwEncodeError($code, $data); } if (null === $encodeFlags) { $encodeFlags = self::DEFAULT_JSON_FLAGS; } $json = json_encode($data, $encodeFlags); if ($json === false) { self::throwEncodeError(json_last_error(), $data); } return $json; } /** * @internal */ public static function pcreLastErrorMessage(int $code): string { if (PHP_VERSION_ID >= 80000) { return preg_last_error_msg(); } $constants = (get_defined_constants(true))['pcre']; $constants = array_filter($constants, function ($key) { return substr($key, -6) == '_ERROR'; }, ARRAY_FILTER_USE_KEY); $constants = array_flip($constants); return $constants[$code] ?? 'UNDEFINED_ERROR'; } /** * Throws an exception according to a given code with a customized message * * @param int $code return code of json_last_error function * @param mixed $data data that was meant to be encoded * @throws \RuntimeException */ private static function throwEncodeError(int $code, $data): never { $msg = match ($code) { JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', default => 'Unknown error', }; throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); } /** * Detect invalid UTF-8 string characters and convert to valid UTF-8. * * Valid UTF-8 input will be left unmodified, but strings containing * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed * original encoding of ISO-8859-15. This conversion may result in * incorrect output if the actual encoding was not ISO-8859-15, but it * will be clean UTF-8 output and will not rely on expensive and fragile * detection algorithms. * * Function converts the input in place in the passed variable so that it * can be used as a callback for array_walk_recursive. * * @param mixed $data Input to check and convert if needed, passed by ref */ private static function detectAndCleanUtf8(&$data): void { if (is_string($data) && preg_match('//u', $data) !== 1) { $data = preg_replace_callback( '/[\x80-\xFF]+/', function (array $m): string { return function_exists('mb_convert_encoding') ? mb_convert_encoding($m[0], 'UTF-8', 'ISO-8859-1') : utf8_encode($m[0]); }, $data ); if (!is_string($data)) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Failed to preg_replace_callback: ' . $pcreErrorCode . ' / ' . self::pcreLastErrorMessage($pcreErrorCode)); } $data = str_replace( ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], $data ); } } /** * Converts a string with a valid 'memory_limit' format, to bytes. * * @param string|false $val * @return int|false Returns an integer representing bytes. Returns FALSE in case of error. */ public static function expandIniShorthandBytes($val) { if (!is_string($val)) { return false; } // support -1 if ((int) $val < 0) { return (int) $val; } if (preg_match('/^\s*(?<val>\d+)(?:\.\d+)?\s*(?<unit>[gmk]?)\s*$/i', $val, $match) !== 1) { return false; } $val = (int) $match['val']; switch (strtolower($match['unit'] ?? '')) { case 'g': $val *= 1024; // no break case 'm': $val *= 1024; // no break case 'k': $val *= 1024; } return $val; } public static function getRecordMessageForException(LogRecord $record): string { $context = ''; $extra = ''; try { if (\count($record->context) > 0) { $context = "\nContext: " . json_encode($record->context, JSON_THROW_ON_ERROR); } if (\count($record->extra) > 0) { $extra = "\nExtra: " . json_encode($record->extra, JSON_THROW_ON_ERROR); } } catch (\Throwable $e) { // noop } return "\nThe exception occurred while attempting to log: " . $record->message . $context . $extra; } } ErrorHandler.php 0000644 00000024000 15107322012 0007631 0 ustar 00 <?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; use Closure; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * Monolog error handler * * A facility to enable logging of runtime errors, exceptions and fatal errors. * * Quick setup: <code>ErrorHandler::register($logger);</code> * * @author Jordi Boggiano <j.boggiano@seld.be> */ class ErrorHandler { private Closure|null $previousExceptionHandler = null; /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */ private array $uncaughtExceptionLevelMap = []; /** @var Closure|true|null */ private Closure|bool|null $previousErrorHandler = null; /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */ private array $errorLevelMap = []; private bool $handleOnlyReportedErrors = true; private bool $hasFatalErrorHandler = false; private string $fatalLevel = LogLevel::ALERT; private string|null $reservedMemory = null; /** @var ?array{type: int, message: string, file: string, line: int, trace: mixed} */ private array|null $lastFatalData = null; private const FATAL_ERRORS = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; public function __construct( private LoggerInterface $logger ) { } /** * Registers a new ErrorHandler for a given Logger * * By default it will handle errors, exceptions and fatal errors * * @param array<int, LogLevel::*>|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling * @param array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling * @return static */ public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self { /** @phpstan-ignore-next-line */ $handler = new static($logger); if ($errorLevelMap !== false) { $handler->registerErrorHandler($errorLevelMap); } if ($exceptionLevelMap !== false) { $handler->registerExceptionHandler($exceptionLevelMap); } if ($fatalLevel !== false) { $handler->registerFatalHandler($fatalLevel); } return $handler; } /** * @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping * @return $this */ public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self { $prev = set_exception_handler(function (\Throwable $e): void { $this->handleException($e); }); $this->uncaughtExceptionLevelMap = $levelMap; foreach ($this->defaultExceptionLevelMap() as $class => $level) { if (!isset($this->uncaughtExceptionLevelMap[$class])) { $this->uncaughtExceptionLevelMap[$class] = $level; } } if ($callPrevious && null !== $prev) { $this->previousExceptionHandler = $prev(...); } return $this; } /** * @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping * @return $this */ public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self { $prev = set_error_handler($this->handleError(...), $errorTypes); $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap); if ($callPrevious) { $this->previousErrorHandler = $prev !== null ? $prev(...) : true; } else { $this->previousErrorHandler = null; } $this->handleOnlyReportedErrors = $handleOnlyReportedErrors; return $this; } /** * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT * @param int $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done * @return $this */ public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self { register_shutdown_function($this->handleFatalError(...)); $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); $this->fatalLevel = null === $level ? LogLevel::ALERT : $level; $this->hasFatalErrorHandler = true; return $this; } /** * @return array<class-string, LogLevel::*> */ protected function defaultExceptionLevelMap(): array { return [ 'ParseError' => LogLevel::CRITICAL, 'Throwable' => LogLevel::ERROR, ]; } /** * @return array<int, LogLevel::*> */ protected function defaultErrorLevelMap(): array { return [ E_ERROR => LogLevel::CRITICAL, E_WARNING => LogLevel::WARNING, E_PARSE => LogLevel::ALERT, E_NOTICE => LogLevel::NOTICE, E_CORE_ERROR => LogLevel::CRITICAL, E_CORE_WARNING => LogLevel::WARNING, E_COMPILE_ERROR => LogLevel::ALERT, E_COMPILE_WARNING => LogLevel::WARNING, E_USER_ERROR => LogLevel::ERROR, E_USER_WARNING => LogLevel::WARNING, E_USER_NOTICE => LogLevel::NOTICE, E_STRICT => LogLevel::NOTICE, E_RECOVERABLE_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::NOTICE, E_USER_DEPRECATED => LogLevel::NOTICE, ]; } private function handleException(\Throwable $e): never { $level = LogLevel::ERROR; foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) { if ($e instanceof $class) { $level = $candidate; break; } } $this->logger->log( $level, sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e] ); if (null !== $this->previousExceptionHandler) { ($this->previousExceptionHandler)($e); } if (!headers_sent() && in_array(strtolower((string) ini_get('display_errors')), ['0', '', 'false', 'off', 'none', 'no'], true)) { http_response_code(500); } exit(255); } private function handleError(int $code, string $message, string $file = '', int $line = 0): bool { if ($this->handleOnlyReportedErrors && 0 === (error_reporting() & $code)) { return false; } // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries if (!$this->hasFatalErrorHandler || !in_array($code, self::FATAL_ERRORS, true)) { $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL; $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]); } else { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); array_shift($trace); // Exclude handleError from trace $this->lastFatalData = ['type' => $code, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $trace]; } if ($this->previousErrorHandler === true) { return false; } if ($this->previousErrorHandler instanceof Closure) { return (bool) ($this->previousErrorHandler)($code, $message, $file, $line); } return true; } /** * @private */ public function handleFatalError(): void { $this->reservedMemory = ''; if (is_array($this->lastFatalData)) { $lastError = $this->lastFatalData; } else { $lastError = error_get_last(); } if (is_array($lastError) && in_array($lastError['type'], self::FATAL_ERRORS, true)) { $trace = $lastError['trace'] ?? null; $this->logger->log( $this->fatalLevel, 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $trace] ); if ($this->logger instanceof Logger) { foreach ($this->logger->getHandlers() as $handler) { $handler->close(); } } } } private static function codeToString(int $code): string { return match ($code) { E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING', E_PARSE => 'E_PARSE', E_NOTICE => 'E_NOTICE', E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING', E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING', E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING', E_USER_NOTICE => 'E_USER_NOTICE', E_STRICT => 'E_STRICT', E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', E_DEPRECATED => 'E_DEPRECATED', E_USER_DEPRECATED => 'E_USER_DEPRECATED', default => 'Unknown PHP error', }; } } Attribute/WithMonologChannel.php 0000644 00000001423 15107322012 0012750 0 ustar 00 <?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\Attribute; /** * A reusable attribute to help configure a class as expecting a given logger channel. * * Using it offers no guarantee: it needs to be leveraged by a Monolog third-party consumer. * * Using it with the Monolog library only has no effect at all: wiring the logger instance into * other classes is not managed by Monolog. */ #[\Attribute(\Attribute::TARGET_CLASS)] final class WithMonologChannel { public function __construct( public readonly string $channel ) { } } Attribute/AsMonologProcessor.php 0000644 00000002644 15107322012 0013015 0 ustar 00 <?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\Attribute; /** * A reusable attribute to help configure a class or a method as a processor. * * Using it offers no guarantee: it needs to be leveraged by a Monolog third-party consumer. * * Using it with the Monolog library only has no effect at all: processors should still be turned into a callable if * needed and manually pushed to the loggers and to the processable handlers. */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class AsMonologProcessor { /** * @param string|null $channel The logging channel the processor should be pushed to. * @param string|null $handler The handler the processor should be pushed to. * @param string|null $method The method that processes the records (if the attribute is used at the class level). * @param int|null $priority The priority of the processor so the order can be determined. */ public function __construct( public readonly ?string $channel = null, public readonly ?string $handler = null, public readonly ?string $method = null, public readonly ?int $priority = null ) { } } Formatter/ScalarFormatter.php 0000644 00000002160 15107322012 0012301 0 ustar 00 <?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\Formatter; use Monolog\LogRecord; /** * Formats data into an associative array of scalar (+ null) values. * Objects and arrays will be JSON encoded. * * @author Andrew Lawson <adlawson@gmail.com> */ class ScalarFormatter extends NormalizerFormatter { /** * @inheritDoc * * @phpstan-return array<string, scalar|null> $record */ public function format(LogRecord $record): array { $result = []; foreach ($record->toArray() as $key => $value) { $result[$key] = $this->toScalar($value); } return $result; } protected function toScalar(mixed $value): string|int|float|bool|null { $normalized = $this->normalize($value); if (is_array($normalized)) { return $this->toJson($normalized, true); } return $normalized; } } Formatter/GelfMessageFormatter.php 0000644 00000011174 15107322013 0013264 0 ustar 00 <?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\Formatter; use Monolog\Level; use Gelf\Message; use Monolog\Utils; use Monolog\LogRecord; /** * Serializes a log message to GELF * @see http://docs.graylog.org/en/latest/pages/gelf.html * * @author Matt Lehner <mlehner@gmail.com> */ class GelfMessageFormatter extends NormalizerFormatter { protected const DEFAULT_MAX_LENGTH = 32766; /** * @var string the name of the system for the Gelf log message */ protected string $systemName; /** * @var string a prefix for 'extra' fields from the Monolog record (optional) */ protected string $extraPrefix; /** * @var string a prefix for 'context' fields from the Monolog record (optional) */ protected string $contextPrefix; /** * @var int max length per field */ protected int $maxLength; /** * Translates Monolog log levels to Graylog2 log priorities. */ private function getGraylog2Priority(Level $level): int { return match ($level) { Level::Debug => 7, Level::Info => 6, Level::Notice => 5, Level::Warning => 4, Level::Error => 3, Level::Critical => 2, Level::Alert => 1, Level::Emergency => 0, }; } /** * @throws \RuntimeException */ public function __construct(?string $systemName = null, ?string $extraPrefix = null, string $contextPrefix = 'ctxt_', ?int $maxLength = null) { if (!class_exists(Message::class)) { throw new \RuntimeException('Composer package graylog2/gelf-php is required to use Monolog\'s GelfMessageFormatter'); } parent::__construct('U.u'); $this->systemName = (null === $systemName || $systemName === '') ? (string) gethostname() : $systemName; $this->extraPrefix = null === $extraPrefix ? '' : $extraPrefix; $this->contextPrefix = $contextPrefix; $this->maxLength = null === $maxLength ? self::DEFAULT_MAX_LENGTH : $maxLength; } /** * @inheritDoc */ public function format(LogRecord $record): Message { $context = $extra = []; if (isset($record->context)) { /** @var mixed[] $context */ $context = parent::normalize($record->context); } if (isset($record->extra)) { /** @var mixed[] $extra */ $extra = parent::normalize($record->extra); } $message = new Message(); $message ->setTimestamp($record->datetime) ->setShortMessage($record->message) ->setHost($this->systemName) ->setLevel($this->getGraylog2Priority($record->level)); // message length + system name length + 200 for padding / metadata $len = 200 + strlen($record->message) + strlen($this->systemName); if ($len > $this->maxLength) { $message->setShortMessage(Utils::substr($record->message, 0, $this->maxLength)); } if (isset($record->channel)) { $message->setAdditional('facility', $record->channel); } foreach ($extra as $key => $val) { $val = is_scalar($val) || null === $val ? $val : $this->toJson($val); $len = strlen($this->extraPrefix . $key . $val); if ($len > $this->maxLength) { $message->setAdditional($this->extraPrefix . $key, Utils::substr((string) $val, 0, $this->maxLength)); continue; } $message->setAdditional($this->extraPrefix . $key, $val); } foreach ($context as $key => $val) { $val = is_scalar($val) || null === $val ? $val : $this->toJson($val); $len = strlen($this->contextPrefix . $key . $val); if ($len > $this->maxLength) { $message->setAdditional($this->contextPrefix . $key, Utils::substr((string) $val, 0, $this->maxLength)); continue; } $message->setAdditional($this->contextPrefix . $key, $val); } if (!$message->hasAdditional('file') && isset($context['exception']['file'])) { if (1 === preg_match("/^(.+):([0-9]+)$/", $context['exception']['file'], $matches)) { $message->setAdditional('file', $matches[1]); $message->setAdditional('line', $matches[2]); } } return $message; } } Formatter/JsonFormatter.php 0000644 00000013614 15107322013 0012014 0 ustar 00 <?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\Formatter; use Stringable; use Throwable; use Monolog\LogRecord; /** * Encodes whatever record data is passed to it as json * * This can be useful to log to databases or remote APIs * * @author Jordi Boggiano <j.boggiano@seld.be> */ class JsonFormatter extends NormalizerFormatter { public const BATCH_MODE_JSON = 1; public const BATCH_MODE_NEWLINES = 2; /** @var self::BATCH_MODE_* */ protected int $batchMode; protected bool $appendNewline; protected bool $ignoreEmptyContextAndExtra; protected bool $includeStacktraces = false; /** * @param self::BATCH_MODE_* $batchMode * * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(int $batchMode = self::BATCH_MODE_JSON, bool $appendNewline = true, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false) { $this->batchMode = $batchMode; $this->appendNewline = $appendNewline; $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; $this->includeStacktraces = $includeStacktraces; parent::__construct(); } /** * The batch mode option configures the formatting style for * multiple records. By default, multiple records will be * formatted as a JSON-encoded array. However, for * compatibility with some API endpoints, alternative styles * are available. */ public function getBatchMode(): int { return $this->batchMode; } /** * True if newlines are appended to every formatted record */ public function isAppendingNewlines(): bool { return $this->appendNewline; } /** * @inheritDoc */ public function format(LogRecord $record): string { $normalized = parent::format($record); if (isset($normalized['context']) && $normalized['context'] === []) { if ($this->ignoreEmptyContextAndExtra) { unset($normalized['context']); } else { $normalized['context'] = new \stdClass; } } if (isset($normalized['extra']) && $normalized['extra'] === []) { if ($this->ignoreEmptyContextAndExtra) { unset($normalized['extra']); } else { $normalized['extra'] = new \stdClass; } } return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : ''); } /** * @inheritDoc */ public function formatBatch(array $records): string { return match ($this->batchMode) { static::BATCH_MODE_NEWLINES => $this->formatBatchNewlines($records), default => $this->formatBatchJson($records), }; } /** * @return $this */ public function includeStacktraces(bool $include = true): self { $this->includeStacktraces = $include; return $this; } /** * Return a JSON-encoded array of records. * * @phpstan-param LogRecord[] $records */ protected function formatBatchJson(array $records): string { return $this->toJson($this->normalize($records), true); } /** * Use new lines to separate records instead of a * JSON-encoded array. * * @phpstan-param LogRecord[] $records */ protected function formatBatchNewlines(array $records): string { $oldNewline = $this->appendNewline; $this->appendNewline = false; $formatted = array_map(fn (LogRecord $record) => $this->format($record), $records); $this->appendNewline = $oldNewline; return implode("\n", $formatted); } /** * Normalizes given $data. * * @return null|scalar|array<mixed[]|scalar|null|object>|object */ protected function normalize(mixed $data, int $depth = 0): mixed { if ($depth > $this->maxNormalizeDepth) { return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization'; } if (is_array($data)) { $normalized = []; $count = 1; foreach ($data as $key => $value) { if ($count++ > $this->maxNormalizeItemCount) { $normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization'; break; } $normalized[$key] = $this->normalize($value, $depth + 1); } return $normalized; } if (is_object($data)) { if ($data instanceof \DateTimeInterface) { return $this->formatDate($data); } if ($data instanceof Throwable) { return $this->normalizeException($data, $depth); } // if the object has specific json serializability we want to make sure we skip the __toString treatment below if ($data instanceof \JsonSerializable) { return $data; } if ($data instanceof Stringable) { return $data->__toString(); } return $data; } if (is_resource($data)) { return parent::normalize($data); } return $data; } /** * Normalizes given exception with or without its own stack trace based on * `includeStacktraces` property. * * @inheritDoc */ protected function normalizeException(Throwable $e, int $depth = 0): array { $data = parent::normalizeException($e, $depth); if (!$this->includeStacktraces) { unset($data['trace']); } return $data; } } Formatter/WildfireFormatter.php 0000644 00000007737 15107322013 0012661 0 ustar 00 <?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\Formatter; use Monolog\Level; use Monolog\LogRecord; /** * Serializes a log message according to Wildfire's header requirements * * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com> * @author Christophe Coevoet <stof@notk.org> * @author Kirill chEbba Chebunin <iam@chebba.org> */ class WildfireFormatter extends NormalizerFormatter { /** * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format * * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(?string $dateFormat = null) { parent::__construct($dateFormat); // http headers do not like non-ISO-8559-1 characters $this->removeJsonEncodeOption(JSON_UNESCAPED_UNICODE); } /** * Translates Monolog log levels to Wildfire levels. * * @return 'LOG'|'INFO'|'WARN'|'ERROR' */ private function toWildfireLevel(Level $level): string { return match ($level) { Level::Debug => 'LOG', Level::Info => 'INFO', Level::Notice => 'INFO', Level::Warning => 'WARN', Level::Error => 'ERROR', Level::Critical => 'ERROR', Level::Alert => 'ERROR', Level::Emergency => 'ERROR', }; } /** * @inheritDoc */ public function format(LogRecord $record): string { // Retrieve the line and file if set and remove them from the formatted extra $file = $line = ''; if (isset($record->extra['file'])) { $file = $record->extra['file']; unset($record->extra['file']); } if (isset($record->extra['line'])) { $line = $record->extra['line']; unset($record->extra['line']); } $message = ['message' => $record->message]; $handleError = false; if (count($record->context) > 0) { $message['context'] = $this->normalize($record->context); $handleError = true; } if (count($record->extra) > 0) { $message['extra'] = $this->normalize($record->extra); $handleError = true; } if (count($message) === 1) { $message = reset($message); } if (is_array($message) && isset($message['context']['table'])) { $type = 'TABLE'; $label = $record->channel .': '. $record->message; $message = $message['context']['table']; } else { $type = $this->toWildfireLevel($record->level); $label = $record->channel; } // Create JSON object describing the appearance of the message in the console $json = $this->toJson([ [ 'Type' => $type, 'File' => $file, 'Line' => $line, 'Label' => $label, ], $message, ], $handleError); // The message itself is a serialization of the above JSON object + it's length return sprintf( '%d|%s|', strlen($json), $json ); } /** * @inheritDoc * * @phpstan-return never */ public function formatBatch(array $records) { throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter'); } /** * @inheritDoc * * @return null|scalar|array<mixed[]|scalar|null>|object */ protected function normalize(mixed $data, int $depth = 0): mixed { if (is_object($data) && !$data instanceof \DateTimeInterface) { return $data; } return parent::normalize($data, $depth); } } Formatter/NormalizerFormatter.php 0000644 00000021762 15107322013 0013230 0 ustar 00 <?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\Formatter; use Monolog\DateTimeImmutable; use Monolog\Utils; use Throwable; use Monolog\LogRecord; /** * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets * * @author Jordi Boggiano <j.boggiano@seld.be> */ class NormalizerFormatter implements FormatterInterface { public const SIMPLE_DATE = "Y-m-d\TH:i:sP"; protected string $dateFormat; protected int $maxNormalizeDepth = 9; protected int $maxNormalizeItemCount = 1000; private int $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS; /** * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(?string $dateFormat = null) { $this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat; if (!function_exists('json_encode')) { throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter'); } } /** * @inheritDoc */ public function format(LogRecord $record) { return $this->normalizeRecord($record); } /** * Normalize an arbitrary value to a scalar|array|null * * @return null|scalar|array<mixed[]|scalar|null> */ public function normalizeValue(mixed $data): mixed { return $this->normalize($data); } /** * @inheritDoc */ public function formatBatch(array $records) { foreach ($records as $key => $record) { $records[$key] = $this->format($record); } return $records; } public function getDateFormat(): string { return $this->dateFormat; } /** * @return $this */ public function setDateFormat(string $dateFormat): self { $this->dateFormat = $dateFormat; return $this; } /** * The maximum number of normalization levels to go through */ public function getMaxNormalizeDepth(): int { return $this->maxNormalizeDepth; } /** * @return $this */ public function setMaxNormalizeDepth(int $maxNormalizeDepth): self { $this->maxNormalizeDepth = $maxNormalizeDepth; return $this; } /** * The maximum number of items to normalize per level */ public function getMaxNormalizeItemCount(): int { return $this->maxNormalizeItemCount; } /** * @return $this */ public function setMaxNormalizeItemCount(int $maxNormalizeItemCount): self { $this->maxNormalizeItemCount = $maxNormalizeItemCount; return $this; } /** * Enables `json_encode` pretty print. * * @return $this */ public function setJsonPrettyPrint(bool $enable): self { if ($enable) { $this->jsonEncodeOptions |= JSON_PRETTY_PRINT; } else { $this->jsonEncodeOptions &= ~JSON_PRETTY_PRINT; } return $this; } /** * Provided as extension point * * Because normalize is called with sub-values of context data etc, normalizeRecord can be * extended when data needs to be appended on the record array but not to other normalized data. * * @return array<mixed[]|scalar|null> */ protected function normalizeRecord(LogRecord $record): array { /** @var array<mixed> $normalized */ $normalized = $this->normalize($record->toArray()); return $normalized; } /** * @return null|scalar|array<mixed[]|scalar|null> */ protected function normalize(mixed $data, int $depth = 0): mixed { if ($depth > $this->maxNormalizeDepth) { return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; } if (null === $data || is_scalar($data)) { if (is_float($data)) { if (is_infinite($data)) { return ($data > 0 ? '' : '-') . 'INF'; } if (is_nan($data)) { return 'NaN'; } } return $data; } if (is_array($data)) { $normalized = []; $count = 1; foreach ($data as $key => $value) { if ($count++ > $this->maxNormalizeItemCount) { $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization'; break; } $normalized[$key] = $this->normalize($value, $depth + 1); } return $normalized; } if ($data instanceof \DateTimeInterface) { return $this->formatDate($data); } if (is_object($data)) { if ($data instanceof Throwable) { return $this->normalizeException($data, $depth); } if ($data instanceof \JsonSerializable) { /** @var null|scalar|array<mixed[]|scalar|null> $value */ $value = $data->jsonSerialize(); } elseif (\get_class($data) === '__PHP_Incomplete_Class') { $accessor = new \ArrayObject($data); $value = (string) $accessor['__PHP_Incomplete_Class_Name']; } elseif (method_exists($data, '__toString')) { /** @var string $value */ $value = $data->__toString(); } else { // the rest is normalized by json encoding and decoding it /** @var null|scalar|array<mixed[]|scalar|null> $value */ $value = json_decode($this->toJson($data, true), true); } return [Utils::getClass($data) => $value]; } if (is_resource($data)) { return sprintf('[resource(%s)]', get_resource_type($data)); } return '[unknown('.gettype($data).')]'; } /** * @return mixed[] */ protected function normalizeException(Throwable $e, int $depth = 0) { if ($depth > $this->maxNormalizeDepth) { return ['Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization']; } if ($e instanceof \JsonSerializable) { return (array) $e->jsonSerialize(); } $data = [ 'class' => Utils::getClass($e), 'message' => $e->getMessage(), 'code' => (int) $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), ]; if ($e instanceof \SoapFault) { if (isset($e->faultcode)) { $data['faultcode'] = $e->faultcode; } if (isset($e->faultactor)) { $data['faultactor'] = $e->faultactor; } if (isset($e->detail)) { if (is_string($e->detail)) { $data['detail'] = $e->detail; } elseif (is_object($e->detail) || is_array($e->detail)) { $data['detail'] = $this->toJson($e->detail, true); } } } $trace = $e->getTrace(); foreach ($trace as $frame) { if (isset($frame['file'], $frame['line'])) { $data['trace'][] = $frame['file'].':'.$frame['line']; } } if (($previous = $e->getPrevious()) instanceof \Throwable) { $data['previous'] = $this->normalizeException($previous, $depth + 1); } return $data; } /** * Return the JSON representation of a value * * @param mixed $data * @throws \RuntimeException if encoding fails and errors are not ignored * @return string if encoding fails and ignoreErrors is true 'null' is returned */ protected function toJson($data, bool $ignoreErrors = false): string { return Utils::jsonEncode($data, $this->jsonEncodeOptions, $ignoreErrors); } protected function formatDate(\DateTimeInterface $date): string { // in case the date format isn't custom then we defer to the custom DateTimeImmutable // formatting logic, which will pick the right format based on whether useMicroseconds is on if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof DateTimeImmutable) { return (string) $date; } return $date->format($this->dateFormat); } /** * @return $this */ public function addJsonEncodeOption(int $option): self { $this->jsonEncodeOptions |= $option; return $this; } /** * @return $this */ public function removeJsonEncodeOption(int $option): self { $this->jsonEncodeOptions &= ~$option; return $this; } } Formatter/LineFormatter.php 0000644 00000021621 15107322014 0011770 0 ustar 00 <?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\Formatter; use Closure; use Monolog\Utils; use Monolog\LogRecord; /** * Formats incoming records into a one-line string * * This is especially useful for logging to files * * @author Jordi Boggiano <j.boggiano@seld.be> * @author Christophe Coevoet <stof@notk.org> */ class LineFormatter extends NormalizerFormatter { public const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; protected string $format; protected bool $allowInlineLineBreaks; protected bool $ignoreEmptyContextAndExtra; protected bool $includeStacktraces; protected ?int $maxLevelNameLength = null; protected string $indentStacktraces = ''; protected Closure|null $stacktracesParser = null; /** * @param string|null $format The format of the message * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries * * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false) { $this->format = $format === null ? static::SIMPLE_FORMAT : $format; $this->allowInlineLineBreaks = $allowInlineLineBreaks; $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; $this->includeStacktraces($includeStacktraces); parent::__construct($dateFormat); } /** * @return $this */ public function includeStacktraces(bool $include = true, ?Closure $parser = null): self { $this->includeStacktraces = $include; if ($this->includeStacktraces) { $this->allowInlineLineBreaks = true; $this->stacktracesParser = $parser; } return $this; } /** * Indent stack traces to separate them a bit from the main log record messages * * @param string $indent The string used to indent, for example " " * @return $this */ public function indentStacktraces(string $indent): self { $this->indentStacktraces = $indent; return $this; } /** * @return $this */ public function allowInlineLineBreaks(bool $allow = true): self { $this->allowInlineLineBreaks = $allow; return $this; } /** * @return $this */ public function ignoreEmptyContextAndExtra(bool $ignore = true): self { $this->ignoreEmptyContextAndExtra = $ignore; return $this; } /** * Allows cutting the level name to get fixed-length levels like INF for INFO, ERR for ERROR if you set this to 3 for example * * @param int|null $maxLevelNameLength Maximum characters for the level name. Set null for infinite length (default) * @return $this */ public function setMaxLevelNameLength(?int $maxLevelNameLength = null): self { $this->maxLevelNameLength = $maxLevelNameLength; return $this; } /** * @inheritDoc */ public function format(LogRecord $record): string { $vars = parent::format($record); if ($this->maxLevelNameLength !== null) { $vars['level_name'] = substr($vars['level_name'], 0, $this->maxLevelNameLength); } $output = $this->format; foreach ($vars['extra'] as $var => $val) { if (false !== strpos($output, '%extra.'.$var.'%')) { $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output); unset($vars['extra'][$var]); } } foreach ($vars['context'] as $var => $val) { if (false !== strpos($output, '%context.'.$var.'%')) { $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output); unset($vars['context'][$var]); } } if ($this->ignoreEmptyContextAndExtra) { if (\count($vars['context']) === 0) { unset($vars['context']); $output = str_replace('%context%', '', $output); } if (\count($vars['extra']) === 0) { unset($vars['extra']); $output = str_replace('%extra%', '', $output); } } foreach ($vars as $var => $val) { if (false !== strpos($output, '%'.$var.'%')) { $output = str_replace('%'.$var.'%', $this->stringify($val), $output); } } // remove leftover %extra.xxx% and %context.xxx% if any if (false !== strpos($output, '%')) { $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); if (null === $output) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); } } return $output; } public function formatBatch(array $records): string { $message = ''; foreach ($records as $record) { $message .= $this->format($record); } return $message; } /** * @param mixed $value */ public function stringify($value): string { return $this->replaceNewlines($this->convertToString($value)); } protected function normalizeException(\Throwable $e, int $depth = 0): string { $str = $this->formatException($e); if (($previous = $e->getPrevious()) instanceof \Throwable) { do { $depth++; if ($depth > $this->maxNormalizeDepth) { $str .= "\n[previous exception] Over " . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; break; } $str .= "\n[previous exception] " . $this->formatException($previous); } while ($previous = $previous->getPrevious()); } return $str; } /** * @param mixed $data */ protected function convertToString($data): string { if (null === $data || is_bool($data)) { return var_export($data, true); } if (is_scalar($data)) { return (string) $data; } return $this->toJson($data, true); } protected function replaceNewlines(string $str): string { if ($this->allowInlineLineBreaks) { if (0 === strpos($str, '{') || 0 === strpos($str, '[')) { $str = preg_replace('/(?<!\\\\)\\\\[rn]/', "\n", $str); if (null === $str) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); } } return $str; } return str_replace(["\r\n", "\r", "\n"], ' ', $str); } private function formatException(\Throwable $e): string { $str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode(); if ($e instanceof \SoapFault) { if (isset($e->faultcode)) { $str .= ' faultcode: ' . $e->faultcode; } if (isset($e->faultactor)) { $str .= ' faultactor: ' . $e->faultactor; } if (isset($e->detail)) { if (is_string($e->detail)) { $str .= ' detail: ' . $e->detail; } elseif (is_object($e->detail) || is_array($e->detail)) { $str .= ' detail: ' . $this->toJson($e->detail, true); } } } $str .= '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')'; if ($this->includeStacktraces) { $str .= $this->stacktracesParser($e); } return $str; } private function stacktracesParser(\Throwable $e): string { $trace = $e->getTraceAsString(); if ($this->stacktracesParser !== null) { $trace = $this->stacktracesParserCustom($trace); } if ($this->indentStacktraces !== '') { $trace = str_replace("\n", "\n{$this->indentStacktraces}", $trace); } return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . $trace . "\n"; } private function stacktracesParserCustom(string $trace): string { return implode("\n", array_filter(array_map($this->stacktracesParser, explode("\n", $trace)))); } } Formatter/ElasticaFormatter.php 0000644 00000004060 15107322014 0012624 0 ustar 00 <?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\Formatter; use Elastica\Document; use Monolog\LogRecord; /** * Format a log message into an Elastica Document * * @author Jelle Vink <jelle.vink@gmail.com> */ class ElasticaFormatter extends NormalizerFormatter { /** * @var string Elastic search index name */ protected string $index; /** * @var string|null Elastic search document type */ protected string|null $type; /** * @param string $index Elastic Search index name * @param ?string $type Elastic Search document type, deprecated as of Elastica 7 * * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(string $index, ?string $type) { // elasticsearch requires a ISO 8601 format date with optional millisecond precision. parent::__construct('Y-m-d\TH:i:s.uP'); $this->index = $index; $this->type = $type; } /** * @inheritDoc */ public function format(LogRecord $record) { $record = parent::format($record); return $this->getDocument($record); } public function getIndex(): string { return $this->index; } /** * @deprecated since Elastica 7 type has no effect */ public function getType(): string { /** @phpstan-ignore-next-line */ return $this->type; } /** * Convert a log message into an Elastica Document * * @param mixed[] $record */ protected function getDocument(array $record): Document { $document = new Document(); $document->setData($record); if (method_exists($document, 'setType')) { $document->setType($this->type); } $document->setIndex($this->index); return $document; } } Formatter/HtmlFormatter.php 0000644 00000011020 15107322014 0011775 0 ustar 00 <?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\Formatter; use Monolog\Level; use Monolog\Utils; use Monolog\LogRecord; /** * Formats incoming records into an HTML table * * This is especially useful for html email logging * * @author Tiago Brito <tlfbrito@gmail.com> */ class HtmlFormatter extends NormalizerFormatter { /** * Translates Monolog log levels to html color priorities. */ protected function getLevelColor(Level $level): string { return match ($level) { Level::Debug => '#CCCCCC', Level::Info => '#28A745', Level::Notice => '#17A2B8', Level::Warning => '#FFC107', Level::Error => '#FD7E14', Level::Critical => '#DC3545', Level::Alert => '#821722', Level::Emergency => '#000000', }; } /** * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(?string $dateFormat = null) { parent::__construct($dateFormat); } /** * Creates an HTML table row * * @param string $th Row header content * @param string $td Row standard cell content * @param bool $escapeTd false if td content must not be html escaped */ protected function addRow(string $th, string $td = ' ', bool $escapeTd = true): string { $th = htmlspecialchars($th, ENT_NOQUOTES, 'UTF-8'); if ($escapeTd) { $td = '<pre>'.htmlspecialchars($td, ENT_NOQUOTES, 'UTF-8').'</pre>'; } return "<tr style=\"padding: 4px;text-align: left;\">\n<th style=\"vertical-align: top;background: #ccc;color: #000\" width=\"100\">$th:</th>\n<td style=\"padding: 4px;text-align: left;vertical-align: top;background: #eee;color: #000\">".$td."</td>\n</tr>"; } /** * Create a HTML h1 tag * * @param string $title Text to be in the h1 */ protected function addTitle(string $title, Level $level): string { $title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8'); return '<h1 style="background: '.$this->getLevelColor($level).';color: #ffffff;padding: 5px;" class="monolog-output">'.$title.'</h1>'; } /** * Formats a log record. * * @return string The formatted record */ public function format(LogRecord $record): string { $output = $this->addTitle($record->level->getName(), $record->level); $output .= '<table cellspacing="1" width="100%" class="monolog-output">'; $output .= $this->addRow('Message', $record->message); $output .= $this->addRow('Time', $this->formatDate($record->datetime)); $output .= $this->addRow('Channel', $record->channel); if (\count($record->context) > 0) { $embeddedTable = '<table cellspacing="1" width="100%">'; foreach ($record->context as $key => $value) { $embeddedTable .= $this->addRow((string) $key, $this->convertToString($value)); } $embeddedTable .= '</table>'; $output .= $this->addRow('Context', $embeddedTable, false); } if (\count($record->extra) > 0) { $embeddedTable = '<table cellspacing="1" width="100%">'; foreach ($record->extra as $key => $value) { $embeddedTable .= $this->addRow((string) $key, $this->convertToString($value)); } $embeddedTable .= '</table>'; $output .= $this->addRow('Extra', $embeddedTable, false); } return $output.'</table>'; } /** * Formats a set of log records. * * @return string The formatted set of records */ public function formatBatch(array $records): string { $message = ''; foreach ($records as $record) { $message .= $this->format($record); } return $message; } /** * @param mixed $data */ protected function convertToString($data): string { if (null === $data || is_scalar($data)) { return (string) $data; } $data = $this->normalize($data); return Utils::jsonEncode($data, JSON_PRETTY_PRINT | Utils::DEFAULT_JSON_FLAGS, true); } } Formatter/ChromePHPFormatter.php 0000644 00000004331 15107322015 0012666 0 ustar 00 <?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\Formatter; use Monolog\Level; use Monolog\LogRecord; /** * Formats a log message according to the ChromePHP array format * * @author Christophe Coevoet <stof@notk.org> */ class ChromePHPFormatter implements FormatterInterface { /** * Translates Monolog log levels to Wildfire levels. * * @return 'log'|'info'|'warn'|'error' */ private function toWildfireLevel(Level $level): string { return match ($level) { Level::Debug => 'log', Level::Info => 'info', Level::Notice => 'info', Level::Warning => 'warn', Level::Error => 'error', Level::Critical => 'error', Level::Alert => 'error', Level::Emergency => 'error', }; } /** * @inheritDoc */ public function format(LogRecord $record) { // Retrieve the line and file if set and remove them from the formatted extra $backtrace = 'unknown'; if (isset($record->extra['file'], $record->extra['line'])) { $backtrace = $record->extra['file'].' : '.$record->extra['line']; unset($record->extra['file'], $record->extra['line']); } $message = ['message' => $record->message]; if (\count($record->context) > 0) { $message['context'] = $record->context; } if (\count($record->extra) > 0) { $message['extra'] = $record->extra; } if (count($message) === 1) { $message = reset($message); } return [ $record->channel, $message, $backtrace, $this->toWildfireLevel($record->level), ]; } /** * @inheritDoc */ public function formatBatch(array $records) { $formatted = []; foreach ($records as $record) { $formatted[] = $this->format($record); } return $formatted; } } Formatter/MongoDBFormatter.php 0000644 00000011467 15107322015 0012376 0 ustar 00 <?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\Formatter; use MongoDB\BSON\Type; use MongoDB\BSON\UTCDateTime; use Monolog\Utils; use Monolog\LogRecord; /** * Formats a record for use with the MongoDBHandler. * * @author Florian Plattner <me@florianplattner.de> */ class MongoDBFormatter implements FormatterInterface { private bool $exceptionTraceAsString; private int $maxNestingLevel; private bool $isLegacyMongoExt; /** * @param int $maxNestingLevel 0 means infinite nesting, the $record itself is level 1, $record->context is 2 * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings */ public function __construct(int $maxNestingLevel = 3, bool $exceptionTraceAsString = true) { $this->maxNestingLevel = max($maxNestingLevel, 0); $this->exceptionTraceAsString = $exceptionTraceAsString; $this->isLegacyMongoExt = extension_loaded('mongodb') && version_compare((string) phpversion('mongodb'), '1.1.9', '<='); } /** * @inheritDoc * * @return mixed[] */ public function format(LogRecord $record): array { /** @var mixed[] $res */ $res = $this->formatArray($record->toArray()); return $res; } /** * @inheritDoc * * @return array<mixed[]> */ public function formatBatch(array $records): array { $formatted = []; foreach ($records as $key => $record) { $formatted[$key] = $this->format($record); } return $formatted; } /** * @param mixed[] $array * @return mixed[]|string Array except when max nesting level is reached then a string "[...]" */ protected function formatArray(array $array, int $nestingLevel = 0) { if ($this->maxNestingLevel > 0 && $nestingLevel > $this->maxNestingLevel) { return '[...]'; } foreach ($array as $name => $value) { if ($value instanceof \DateTimeInterface) { $array[$name] = $this->formatDate($value, $nestingLevel + 1); } elseif ($value instanceof \Throwable) { $array[$name] = $this->formatException($value, $nestingLevel + 1); } elseif (is_array($value)) { $array[$name] = $this->formatArray($value, $nestingLevel + 1); } elseif (is_object($value) && !$value instanceof Type) { $array[$name] = $this->formatObject($value, $nestingLevel + 1); } } return $array; } /** * @param mixed $value * @return mixed[]|string */ protected function formatObject($value, int $nestingLevel) { $objectVars = get_object_vars($value); $objectVars['class'] = Utils::getClass($value); return $this->formatArray($objectVars, $nestingLevel); } /** * @return mixed[]|string */ protected function formatException(\Throwable $exception, int $nestingLevel) { $formattedException = [ 'class' => Utils::getClass($exception), 'message' => $exception->getMessage(), 'code' => (int) $exception->getCode(), 'file' => $exception->getFile() . ':' . $exception->getLine(), ]; if ($this->exceptionTraceAsString === true) { $formattedException['trace'] = $exception->getTraceAsString(); } else { $formattedException['trace'] = $exception->getTrace(); } return $this->formatArray($formattedException, $nestingLevel); } protected function formatDate(\DateTimeInterface $value, int $nestingLevel): UTCDateTime { if ($this->isLegacyMongoExt) { return $this->legacyGetMongoDbDateTime($value); } return $this->getMongoDbDateTime($value); } private function getMongoDbDateTime(\DateTimeInterface $value): UTCDateTime { return new UTCDateTime((int) floor(((float) $value->format('U.u')) * 1000)); } /** * This is needed to support MongoDB Driver v1.19 and below * * See https://github.com/mongodb/mongo-php-driver/issues/426 * * It can probably be removed in 2.1 or later once MongoDB's 1.2 is released and widely adopted */ private function legacyGetMongoDbDateTime(\DateTimeInterface $value): UTCDateTime { $milliseconds = floor(((float) $value->format('U.u')) * 1000); $milliseconds = (PHP_INT_SIZE == 8) //64-bit OS? ? (int) $milliseconds : (string) $milliseconds; return new UTCDateTime($milliseconds); } } Formatter/FluentdFormatter.php 0000644 00000004451 15107322015 0012505 0 ustar 00 <?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\Formatter; use Monolog\Utils; use Monolog\LogRecord; /** * Class FluentdFormatter * * Serializes a log message to Fluentd unix socket protocol * * Fluentd config: * * <source> * type unix * path /var/run/td-agent/td-agent.sock * </source> * * Monolog setup: * * $logger = new Monolog\Logger('fluent.tag'); * $fluentHandler = new Monolog\Handler\SocketHandler('unix:///var/run/td-agent/td-agent.sock'); * $fluentHandler->setFormatter(new Monolog\Formatter\FluentdFormatter()); * $logger->pushHandler($fluentHandler); * * @author Andrius Putna <fordnox@gmail.com> */ class FluentdFormatter implements FormatterInterface { /** * @var bool $levelTag should message level be a part of the fluentd tag */ protected bool $levelTag = false; /** * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(bool $levelTag = false) { if (!function_exists('json_encode')) { throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s FluentdUnixFormatter'); } $this->levelTag = $levelTag; } public function isUsingLevelsInTag(): bool { return $this->levelTag; } public function format(LogRecord $record): string { $tag = $record->channel; if ($this->levelTag) { $tag .= '.' . $record->level->toPsrLogLevel(); } $message = [ 'message' => $record->message, 'context' => $record->context, 'extra' => $record->extra, ]; if (!$this->levelTag) { $message['level'] = $record->level->value; $message['level_name'] = $record->level->getName(); } return Utils::jsonEncode([$tag, $record->datetime->getTimestamp(), $message]); } public function formatBatch(array $records): string { $message = ''; foreach ($records as $record) { $message .= $this->format($record); } return $message; } } Formatter/LogglyFormatter.php 0000644 00000002400 15107322015 0012331 0 ustar 00 <?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\Formatter; use Monolog\LogRecord; /** * Encodes message information into JSON in a format compatible with Loggly. * * @author Adam Pancutt <adam@pancutt.com> */ class LogglyFormatter extends JsonFormatter { /** * Overrides the default batch mode to new lines for compatibility with the * Loggly bulk API. */ public function __construct(int $batchMode = self::BATCH_MODE_NEWLINES, bool $appendNewline = false) { parent::__construct($batchMode, $appendNewline); } /** * Appends the 'timestamp' parameter for indexing by Loggly. * * @see https://www.loggly.com/docs/automated-parsing/#json * @see \Monolog\Formatter\JsonFormatter::format() */ protected function normalizeRecord(LogRecord $record): array { $recordData = parent::normalizeRecord($record); $recordData["timestamp"] = $record->datetime->format("Y-m-d\TH:i:s.uO"); unset($recordData["datetime"]); return $recordData; } } Formatter/FormatterInterface.php 0000644 00000001546 15107322016 0013007 0 ustar 00 <?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\Formatter; use Monolog\LogRecord; /** * Interface for formatters * * @author Jordi Boggiano <j.boggiano@seld.be> */ interface FormatterInterface { /** * Formats a log record. * * @param LogRecord $record A record to format * @return mixed The formatted record */ public function format(LogRecord $record); /** * Formats a set of log records. * * @param array<LogRecord> $records A set of records to format * @return mixed The formatted set of records */ public function formatBatch(array $records); } Formatter/LogstashFormatter.php 0000644 00000006673 15107322016 0012701 0 ustar 00 <?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\Formatter; use Monolog\LogRecord; /** * Serializes a log message to Logstash Event Format * * @see https://www.elastic.co/products/logstash * @see https://github.com/elastic/logstash/blob/master/logstash-core/src/main/java/org/logstash/Event.java * * @author Tim Mower <timothy.mower@gmail.com> */ class LogstashFormatter extends NormalizerFormatter { /** * @var string the name of the system for the Logstash log message, used to fill the @source field */ protected string $systemName; /** * @var string an application name for the Logstash log message, used to fill the @type field */ protected string $applicationName; /** * @var string the key for 'extra' fields from the Monolog record */ protected string $extraKey; /** * @var string the key for 'context' fields from the Monolog record */ protected string $contextKey; /** * @param string $applicationName The application that sends the data, used as the "type" field of logstash * @param string|null $systemName The system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine * @param string $extraKey The key for extra keys inside logstash "fields", defaults to extra * @param string $contextKey The key for context keys inside logstash "fields", defaults to context * * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(string $applicationName, ?string $systemName = null, string $extraKey = 'extra', string $contextKey = 'context') { // logstash requires a ISO 8601 format date with optional millisecond precision. parent::__construct('Y-m-d\TH:i:s.uP'); $this->systemName = $systemName === null ? (string) gethostname() : $systemName; $this->applicationName = $applicationName; $this->extraKey = $extraKey; $this->contextKey = $contextKey; } /** * @inheritDoc */ public function format(LogRecord $record): string { $recordData = parent::format($record); $message = [ '@timestamp' => $recordData['datetime'], '@version' => 1, 'host' => $this->systemName, ]; if (isset($recordData['message'])) { $message['message'] = $recordData['message']; } if (isset($recordData['channel'])) { $message['type'] = $recordData['channel']; $message['channel'] = $recordData['channel']; } if (isset($recordData['level_name'])) { $message['level'] = $recordData['level_name']; } if (isset($recordData['level'])) { $message['monolog_level'] = $recordData['level']; } if ('' !== $this->applicationName) { $message['type'] = $this->applicationName; } if (\count($recordData['extra']) > 0) { $message[$this->extraKey] = $recordData['extra']; } if (\count($recordData['context']) > 0) { $message[$this->contextKey] = $recordData['context']; } return $this->toJson($message) . "\n"; } } Formatter/FlowdockFormatter.php 0000644 00000004747 15107322016 0012665 0 ustar 00 <?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\Formatter; use Monolog\LogRecord; /** * formats the record to be used in the FlowdockHandler * * @author Dominik Liebler <liebler.dominik@gmail.com> * @deprecated Since 2.9.0 and 3.3.0, Flowdock was shutdown we will thus drop this handler in Monolog 4 */ class FlowdockFormatter implements FormatterInterface { private string $source; private string $sourceEmail; public function __construct(string $source, string $sourceEmail) { $this->source = $source; $this->sourceEmail = $sourceEmail; } /** * @inheritDoc * * @return mixed[] */ public function format(LogRecord $record): array { $tags = [ '#logs', '#' . $record->level->toPsrLogLevel(), '#' . $record->channel, ]; foreach ($record->extra as $value) { $tags[] = '#' . $value; } $subject = sprintf( 'in %s: %s - %s', $this->source, $record->level->getName(), $this->getShortMessage($record->message) ); return [ 'source' => $this->source, 'from_address' => $this->sourceEmail, 'subject' => $subject, 'content' => $record->message, 'tags' => $tags, 'project' => $this->source, ]; } /** * @inheritDoc * * @return mixed[][] */ public function formatBatch(array $records): array { $formatted = []; foreach ($records as $record) { $formatted[] = $this->format($record); } return $formatted; } public function getShortMessage(string $message): string { static $hasMbString; if (null === $hasMbString) { $hasMbString = function_exists('mb_strlen'); } $maxLength = 45; if ($hasMbString) { if (mb_strlen($message, 'UTF-8') > $maxLength) { $message = mb_substr($message, 0, $maxLength - 4, 'UTF-8') . ' ...'; } } else { if (strlen($message) > $maxLength) { $message = substr($message, 0, $maxLength - 4) . ' ...'; } } return $message; } } Formatter/LogmaticFormatter.php 0000644 00000003045 15107322016 0012642 0 ustar 00 <?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\Formatter; use Monolog\LogRecord; /** * Encodes message information into JSON in a format compatible with Logmatic. * * @author Julien Breux <julien.breux@gmail.com> */ class LogmaticFormatter extends JsonFormatter { protected const MARKERS = ["sourcecode", "php"]; protected string $hostname = ''; protected string $appName = ''; /** * @return $this */ public function setHostname(string $hostname): self { $this->hostname = $hostname; return $this; } /** * @return $this */ public function setAppName(string $appName): self { $this->appName = $appName; return $this; } /** * Appends the 'hostname' and 'appname' parameter for indexing by Logmatic. * * @see http://doc.logmatic.io/docs/basics-to-send-data * @see \Monolog\Formatter\JsonFormatter::format() */ public function normalizeRecord(LogRecord $record): array { $record = parent::normalizeRecord($record); if ($this->hostname !== '') { $record["hostname"] = $this->hostname; } if ($this->appName !== '') { $record["appname"] = $this->appName; } $record["@marker"] = static::MARKERS; return $record; } } Formatter/SyslogFormatter.php 0000644 00000003605 15107322017 0012366 0 ustar 00 <?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\Formatter; use Monolog\Level; use Monolog\LogRecord; /** * Serializes a log message according to RFC 5424 * * @author Dalibor Karlović <dalibor.karlovic@sigwin.hr> * @author Renat Gabdullin <renatobyj@gmail.com> */ class SyslogFormatter extends LineFormatter { private const SYSLOG_FACILITY_USER = 1; private const FORMAT = "<%extra.priority%>1 %datetime% %extra.hostname% %extra.app-name% %extra.procid% %channel% %extra.structured-data% %level_name%: %message% %context% %extra%\n"; private const NILVALUE = '-'; private string $hostname; private int $procid; public function __construct(private string $applicationName = self::NILVALUE) { parent::__construct(self::FORMAT, 'Y-m-d\TH:i:s.uP', true, true); $this->hostname = (string) gethostname(); $this->procid = (int) getmypid(); } public function format(LogRecord $record): string { $record->extra = $this->formatExtra($record); return parent::format($record); } /** * @param LogRecord $record * @return array<string, mixed> */ private function formatExtra(LogRecord $record): array { $extra = $record->extra; $extra['app-name'] = $this->applicationName; $extra['hostname'] = $this->hostname; $extra['procid'] = $this->procid; $extra['priority'] = self::calculatePriority($record->level); $extra['structured-data'] = self::NILVALUE; return $extra; } private static function calculatePriority(Level $level): int { return (self::SYSLOG_FACILITY_USER * 8) + $level->toRFC5424Level(); } } Formatter/ElasticsearchFormatter.php 0000644 00000003605 15107322017 0013660 0 ustar 00 <?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\Formatter; use DateTimeInterface; use Monolog\LogRecord; /** * Format a log message into an Elasticsearch record * * @author Avtandil Kikabidze <akalongman@gmail.com> */ class ElasticsearchFormatter extends NormalizerFormatter { /** * @var string Elasticsearch index name */ protected string $index; /** * @var string Elasticsearch record type */ protected string $type; /** * @param string $index Elasticsearch index name * @param string $type Elasticsearch record type * * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(string $index, string $type) { // Elasticsearch requires an ISO 8601 format date with optional millisecond precision. parent::__construct(DateTimeInterface::ISO8601); $this->index = $index; $this->type = $type; } /** * @inheritDoc */ public function format(LogRecord $record) { $record = parent::format($record); return $this->getDocument($record); } /** * Getter index */ public function getIndex(): string { return $this->index; } /** * Getter type */ public function getType(): string { return $this->type; } /** * Convert a log message into an Elasticsearch record * * @param mixed[] $record Log message * @return mixed[] */ protected function getDocument(array $record): array { $record['_index'] = $this->index; $record['_type'] = $this->type; return $record; } } Formatter/GoogleCloudLoggingFormatter.php 0000644 00000002241 15107322017 0014613 0 ustar 00 <?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\Formatter; use DateTimeInterface; use Monolog\LogRecord; /** * Encodes message information into JSON in a format compatible with Cloud logging. * * @see https://cloud.google.com/logging/docs/structured-logging * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry * * @author Luís Cobucci <lcobucci@gmail.com> */ final class GoogleCloudLoggingFormatter extends JsonFormatter { protected function normalizeRecord(LogRecord $record): array { $normalized = parent::normalizeRecord($record); // Re-key level for GCP logging $normalized['severity'] = $normalized['level_name']; $normalized['time'] = $record->datetime->format(DateTimeInterface::RFC3339_EXTENDED); // Remove keys that are not used by GCP unset($normalized['level'], $normalized['level_name'], $normalized['datetime']); return $normalized; } } Logger.php 0000644 00000054675 15107322020 0006505 0 ustar 00 <?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; use Closure; use DateTimeZone; use Fiber; use Monolog\Handler\HandlerInterface; use Monolog\Processor\ProcessorInterface; use Psr\Log\LoggerInterface; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Throwable; use Stringable; use WeakMap; /** * Monolog log channel * * It contains a stack of Handlers and a stack of Processors, * and uses them to store records that are added to it. * * @author Jordi Boggiano <j.boggiano@seld.be> * @final */ class Logger implements LoggerInterface, ResettableInterface { /** * Detailed debug information * * @deprecated Use \Monolog\Level::Debug */ public const DEBUG = 100; /** * Interesting events * * Examples: User logs in, SQL logs. * * @deprecated Use \Monolog\Level::Info */ public const INFO = 200; /** * Uncommon events * * @deprecated Use \Monolog\Level::Notice */ public const NOTICE = 250; /** * Exceptional occurrences that are not errors * * Examples: Use of deprecated APIs, poor use of an API, * undesirable things that are not necessarily wrong. * * @deprecated Use \Monolog\Level::Warning */ public const WARNING = 300; /** * Runtime errors * * @deprecated Use \Monolog\Level::Error */ public const ERROR = 400; /** * Critical conditions * * Example: Application component unavailable, unexpected exception. * * @deprecated Use \Monolog\Level::Critical */ public const CRITICAL = 500; /** * Action must be taken immediately * * Example: Entire website down, database unavailable, etc. * This should trigger the SMS alerts and wake you up. * * @deprecated Use \Monolog\Level::Alert */ public const ALERT = 550; /** * Urgent alert. * * @deprecated Use \Monolog\Level::Emergency */ public const EMERGENCY = 600; /** * Monolog API version * * This is only bumped when API breaks are done and should * follow the major version of the library */ public const API = 3; /** * Mapping between levels numbers defined in RFC 5424 and Monolog ones * * @phpstan-var array<int, Level> $rfc_5424_levels */ private const RFC_5424_LEVELS = [ 7 => Level::Debug, 6 => Level::Info, 5 => Level::Notice, 4 => Level::Warning, 3 => Level::Error, 2 => Level::Critical, 1 => Level::Alert, 0 => Level::Emergency, ]; protected string $name; /** * The handler stack * * @var list<HandlerInterface> */ protected array $handlers; /** * Processors that will process all log records * * To process records of a single handler instead, add the processor on that specific handler * * @var array<(callable(LogRecord): LogRecord)|ProcessorInterface> */ protected array $processors; protected bool $microsecondTimestamps = true; protected DateTimeZone $timezone; protected Closure|null $exceptionHandler = null; /** * Keeps track of depth to prevent infinite logging loops */ private int $logDepth = 0; /** * @var WeakMap<Fiber<mixed, mixed, mixed, mixed>, int> Keeps track of depth inside fibers to prevent infinite logging loops */ private WeakMap $fiberLogDepth; /** * Whether to detect infinite logging loops * This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this */ private bool $detectCycles = true; /** * @param string $name The logging channel, a simple descriptive name that is attached to all log records * @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. * @param callable[] $processors Optional array of processors * @param DateTimeZone|null $timezone Optional timezone, if not provided date_default_timezone_get() will be used * * @phpstan-param array<(callable(LogRecord): LogRecord)|ProcessorInterface> $processors */ public function __construct(string $name, array $handlers = [], array $processors = [], DateTimeZone|null $timezone = null) { $this->name = $name; $this->setHandlers($handlers); $this->processors = $processors; $this->timezone = $timezone ?? new DateTimeZone(date_default_timezone_get()); $this->fiberLogDepth = new \WeakMap(); } public function getName(): string { return $this->name; } /** * Return a new cloned instance with the name changed * * @return static */ public function withName(string $name): self { $new = clone $this; $new->name = $name; return $new; } /** * Pushes a handler on to the stack. * * @return $this */ public function pushHandler(HandlerInterface $handler): self { array_unshift($this->handlers, $handler); return $this; } /** * Pops a handler from the stack * * @throws \LogicException If empty handler stack */ public function popHandler(): HandlerInterface { if (0 === \count($this->handlers)) { throw new \LogicException('You tried to pop from an empty handler stack.'); } return array_shift($this->handlers); } /** * Set handlers, replacing all existing ones. * * If a map is passed, keys will be ignored. * * @param list<HandlerInterface> $handlers * @return $this */ public function setHandlers(array $handlers): self { $this->handlers = []; foreach (array_reverse($handlers) as $handler) { $this->pushHandler($handler); } return $this; } /** * @return list<HandlerInterface> */ public function getHandlers(): array { return $this->handlers; } /** * Adds a processor on to the stack. * * @phpstan-param ProcessorInterface|(callable(LogRecord): LogRecord) $callback * @return $this */ public function pushProcessor(ProcessorInterface|callable $callback): self { array_unshift($this->processors, $callback); return $this; } /** * Removes the processor on top of the stack and returns it. * * @phpstan-return ProcessorInterface|(callable(LogRecord): LogRecord) * @throws \LogicException If empty processor stack */ public function popProcessor(): callable { if (0 === \count($this->processors)) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } /** * @return callable[] * @phpstan-return array<ProcessorInterface|(callable(LogRecord): LogRecord)> */ public function getProcessors(): array { return $this->processors; } /** * Control the use of microsecond resolution timestamps in the 'datetime' * member of new records. * * As of PHP7.1 microseconds are always included by the engine, so * there is no performance penalty and Monolog 2 enabled microseconds * by default. This function lets you disable them though in case you want * to suppress microseconds from the output. * * @param bool $micro True to use microtime() to create timestamps * @return $this */ public function useMicrosecondTimestamps(bool $micro): self { $this->microsecondTimestamps = $micro; return $this; } /** * @return $this */ public function useLoggingLoopDetection(bool $detectCycles): self { $this->detectCycles = $detectCycles; return $this; } /** * Adds a log record. * * @param int $level The logging level (a Monolog or RFC 5424 level) * @param string $message The log message * @param mixed[] $context The log context * @param DateTimeImmutable $datetime Optional log date to log into the past or future * @return bool Whether the record has been processed * * @phpstan-param value-of<Level::VALUES>|Level $level */ public function addRecord(int|Level $level, string $message, array $context = [], DateTimeImmutable $datetime = null): bool { if (is_int($level) && isset(self::RFC_5424_LEVELS[$level])) { $level = self::RFC_5424_LEVELS[$level]; } if ($this->detectCycles) { if (null !== ($fiber = Fiber::getCurrent())) { $logDepth = $this->fiberLogDepth[$fiber] = ($this->fiberLogDepth[$fiber] ?? 0) + 1; } else { $logDepth = ++$this->logDepth; } } else { $logDepth = 0; } if ($logDepth === 3) { $this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.'); return false; } elseif ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above return false; } try { $recordInitialized = count($this->processors) === 0; $record = new LogRecord( datetime: $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone), channel: $this->name, level: self::toMonologLevel($level), message: $message, context: $context, extra: [], ); $handled = false; foreach ($this->handlers as $handler) { if (false === $recordInitialized) { // skip initializing the record as long as no handler is going to handle it if (!$handler->isHandling($record)) { continue; } try { foreach ($this->processors as $processor) { $record = $processor($record); } $recordInitialized = true; } catch (Throwable $e) { $this->handleException($e, $record); return true; } } // once the record is initialized, send it to all handlers as long as the bubbling chain is not interrupted try { $handled = true; if (true === $handler->handle(clone $record)) { break; } } catch (Throwable $e) { $this->handleException($e, $record); return true; } } return $handled; } finally { if ($this->detectCycles) { if (isset($fiber)) { $this->fiberLogDepth[$fiber]--; } else { $this->logDepth--; } } } } /** * Ends a log cycle and frees all resources used by handlers. * * Closing a Handler means flushing all buffers and freeing any open resources/handles. * Handlers that have been closed should be able to accept log records again and re-open * themselves on demand, but this may not always be possible depending on implementation. * * This is useful at the end of a request and will be called automatically on every handler * when they get destructed. */ public function close(): void { foreach ($this->handlers as $handler) { $handler->close(); } } /** * Ends a log cycle and resets all handlers and processors to their initial state. * * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal * state, and getting it back to a state in which it can receive log records again. * * This is useful in case you want to avoid logs leaking between two requests or jobs when you * have a long running process like a worker or an application server serving multiple requests * in one process. */ public function reset(): void { foreach ($this->handlers as $handler) { if ($handler instanceof ResettableInterface) { $handler->reset(); } } foreach ($this->processors as $processor) { if ($processor instanceof ResettableInterface) { $processor->reset(); } } } /** * Gets the name of the logging level as a string. * * This still returns a string instead of a Level for BC, but new code should not rely on this method. * * @throws \Psr\Log\InvalidArgumentException If level is not defined * * @phpstan-param value-of<Level::VALUES>|Level $level * @phpstan-return value-of<Level::NAMES> * * @deprecated Since 3.0, use {@see toMonologLevel} or {@see \Monolog\Level->getName()} instead */ public static function getLevelName(int|Level $level): string { return self::toMonologLevel($level)->getName(); } /** * Converts PSR-3 levels to Monolog ones if necessary * * @param int|string|Level|LogLevel::* $level Level number (monolog) or name (PSR-3) * @throws \Psr\Log\InvalidArgumentException If level is not defined * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public static function toMonologLevel(string|int|Level $level): Level { if ($level instanceof Level) { return $level; } if (\is_string($level)) { if (\is_numeric($level)) { $levelEnum = Level::tryFrom((int) $level); if ($levelEnum === null) { throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', Level::NAMES + Level::VALUES)); } return $levelEnum; } // Contains first char of all log levels and avoids using strtoupper() which may have // strange results depending on locale (for example, "i" will become "İ" in Turkish locale) $upper = strtr(substr($level, 0, 1), 'dinweca', 'DINWECA') . strtolower(substr($level, 1)); if (defined(Level::class.'::'.$upper)) { return constant(Level::class . '::' . $upper); } throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', Level::NAMES + Level::VALUES)); } $levelEnum = Level::tryFrom($level); if ($levelEnum === null) { throw new InvalidArgumentException('Level "'.var_export($level, true).'" is not defined, use one of: '.implode(', ', Level::NAMES + Level::VALUES)); } return $levelEnum; } /** * Checks whether the Logger has a handler that listens on the given level * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function isHandling(int|string|Level $level): bool { $record = new LogRecord( datetime: new DateTimeImmutable($this->microsecondTimestamps, $this->timezone), channel: $this->name, message: '', level: self::toMonologLevel($level), ); foreach ($this->handlers as $handler) { if ($handler->isHandling($record)) { return true; } } return false; } /** * Set a custom exception handler that will be called if adding a new record fails * * The Closure will receive an exception object and the record that failed to be logged * * @return $this */ public function setExceptionHandler(Closure|null $callback): self { $this->exceptionHandler = $callback; return $this; } public function getExceptionHandler(): Closure|null { return $this->exceptionHandler; } /** * Adds a log record at an arbitrary level. * * This method allows for compatibility with common interfaces. * * @param mixed $level The log level (a Monolog, PSR-3 or RFC 5424 level) * @param string|Stringable $message The log message * @param mixed[] $context The log context * * @phpstan-param Level|LogLevel::* $level */ public function log($level, string|\Stringable $message, array $context = []): void { if (!$level instanceof Level) { if (!is_string($level) && !is_int($level)) { throw new \InvalidArgumentException('$level is expected to be a string, int or '.Level::class.' instance'); } if (isset(self::RFC_5424_LEVELS[$level])) { $level = self::RFC_5424_LEVELS[$level]; } $level = static::toMonologLevel($level); } $this->addRecord($level, (string) $message, $context); } /** * Adds a log record at the DEBUG level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function debug(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Debug, (string) $message, $context); } /** * Adds a log record at the INFO level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function info(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Info, (string) $message, $context); } /** * Adds a log record at the NOTICE level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function notice(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Notice, (string) $message, $context); } /** * Adds a log record at the WARNING level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function warning(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Warning, (string) $message, $context); } /** * Adds a log record at the ERROR level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function error(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Error, (string) $message, $context); } /** * Adds a log record at the CRITICAL level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function critical(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Critical, (string) $message, $context); } /** * Adds a log record at the ALERT level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function alert(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Alert, (string) $message, $context); } /** * Adds a log record at the EMERGENCY level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function emergency(string|\Stringable $message, array $context = []): void { $this->addRecord(Level::Emergency, (string) $message, $context); } /** * Sets the timezone to be used for the timestamp of log records. * * @return $this */ public function setTimezone(DateTimeZone $tz): self { $this->timezone = $tz; return $this; } /** * Returns the timezone to be used for the timestamp of log records. */ public function getTimezone(): DateTimeZone { return $this->timezone; } /** * Delegates exception management to the custom exception handler, * or throws the exception if no custom handler is set. */ protected function handleException(Throwable $e, LogRecord $record): void { if (null === $this->exceptionHandler) { throw $e; } ($this->exceptionHandler)($e, $record); } /** * @return array<string, mixed> */ public function __serialize(): array { return [ 'name' => $this->name, 'handlers' => $this->handlers, 'processors' => $this->processors, 'microsecondTimestamps' => $this->microsecondTimestamps, 'timezone' => $this->timezone, 'exceptionHandler' => $this->exceptionHandler, 'logDepth' => $this->logDepth, 'detectCycles' => $this->detectCycles, ]; } /** * @param array<string, mixed> $data */ public function __unserialize(array $data): void { foreach (['name', 'handlers', 'processors', 'microsecondTimestamps', 'timezone', 'exceptionHandler', 'logDepth', 'detectCycles'] as $property) { if (isset($data[$property])) { $this->$property = $data[$property]; } } $this->fiberLogDepth = new \WeakMap(); } } LogRecord.php 0000644 00000006442 15107322020 0007133 0 ustar 00 <?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; use ArrayAccess; /** * Monolog log record * * @author Jordi Boggiano <j.boggiano@seld.be> * @template-implements ArrayAccess<'message'|'level'|'context'|'level_name'|'channel'|'datetime'|'extra', int|string|\DateTimeImmutable|array<mixed>> */ class LogRecord implements ArrayAccess { private const MODIFIABLE_FIELDS = [ 'extra' => true, 'formatted' => true, ]; public function __construct( public readonly \DateTimeImmutable $datetime, public readonly string $channel, public readonly Level $level, public readonly string $message, /** @var array<mixed> */ public readonly array $context = [], /** @var array<mixed> */ public array $extra = [], public mixed $formatted = null, ) { } public function offsetSet(mixed $offset, mixed $value): void { if ($offset === 'extra') { if (!is_array($value)) { throw new \InvalidArgumentException('extra must be an array'); } $this->extra = $value; return; } if ($offset === 'formatted') { $this->formatted = $value; return; } throw new \LogicException('Unsupported operation: setting '.$offset); } public function offsetExists(mixed $offset): bool { if ($offset === 'level_name') { return true; } return isset($this->{$offset}); } public function offsetUnset(mixed $offset): void { throw new \LogicException('Unsupported operation'); } public function &offsetGet(mixed $offset): mixed { if ($offset === 'level_name' || $offset === 'level') { // avoid returning readonly props by ref as this is illegal if ($offset === 'level_name') { $copy = $this->level->getName(); } else { $copy = $this->level->value; } return $copy; } if (isset(self::MODIFIABLE_FIELDS[$offset])) { return $this->{$offset}; } // avoid returning readonly props by ref as this is illegal $copy = $this->{$offset}; return $copy; } /** * @phpstan-return array{message: string, context: mixed[], level: value-of<Level::VALUES>, level_name: value-of<Level::NAMES>, channel: string, datetime: \DateTimeImmutable, extra: mixed[]} */ public function toArray(): array { return [ 'message' => $this->message, 'context' => $this->context, 'level' => $this->level->value, 'level_name' => $this->level->getName(), 'channel' => $this->channel, 'datetime' => $this->datetime, 'extra' => $this->extra, ]; } public function with(mixed ...$args): self { foreach (['message', 'context', 'level', 'channel', 'datetime', 'extra'] as $prop) { $args[$prop] ??= $this->{$prop}; } return new self(...$args); } } Registry.php 0000644 00000007666 15107322020 0007074 0 ustar 00 <?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; use InvalidArgumentException; /** * Monolog log registry * * Allows to get `Logger` instances in the global scope * via static method calls on this class. * * <code> * $application = new Monolog\Logger('application'); * $api = new Monolog\Logger('api'); * * Monolog\Registry::addLogger($application); * Monolog\Registry::addLogger($api); * * function testLogger() * { * Monolog\Registry::api()->error('Sent to $api Logger instance'); * Monolog\Registry::application()->error('Sent to $application Logger instance'); * } * </code> * * @author Tomas Tatarko <tomas@tatarko.sk> */ class Registry { /** * List of all loggers in the registry (by named indexes) * * @var Logger[] */ private static array $loggers = []; /** * Adds new logging channel to the registry * * @param Logger $logger Instance of the logging channel * @param string|null $name Name of the logging channel ($logger->getName() by default) * @param bool $overwrite Overwrite instance in the registry if the given name already exists? * @throws \InvalidArgumentException If $overwrite set to false and named Logger instance already exists */ public static function addLogger(Logger $logger, ?string $name = null, bool $overwrite = false): void { $name = $name ?? $logger->getName(); if (isset(self::$loggers[$name]) && !$overwrite) { throw new InvalidArgumentException('Logger with the given name already exists'); } self::$loggers[$name] = $logger; } /** * Checks if such logging channel exists by name or instance * * @param string|Logger $logger Name or logger instance */ public static function hasLogger($logger): bool { if ($logger instanceof Logger) { $index = array_search($logger, self::$loggers, true); return false !== $index; } return isset(self::$loggers[$logger]); } /** * Removes instance from registry by name or instance * * @param string|Logger $logger Name or logger instance */ public static function removeLogger($logger): void { if ($logger instanceof Logger) { if (false !== ($idx = array_search($logger, self::$loggers, true))) { unset(self::$loggers[$idx]); } } else { unset(self::$loggers[$logger]); } } /** * Clears the registry */ public static function clear(): void { self::$loggers = []; } /** * Gets Logger instance from the registry * * @param string $name Name of the requested Logger instance * @throws \InvalidArgumentException If named Logger instance is not in the registry */ public static function getInstance(string $name): Logger { if (!isset(self::$loggers[$name])) { throw new InvalidArgumentException(sprintf('Requested "%s" logger instance is not in the registry', $name)); } return self::$loggers[$name]; } /** * Gets Logger instance from the registry via static method call * * @param string $name Name of the requested Logger instance * @param mixed[] $arguments Arguments passed to static method call * @throws \InvalidArgumentException If named Logger instance is not in the registry * @return Logger Requested instance of Logger */ public static function __callStatic(string $name, array $arguments): Logger { return self::getInstance($name); } } Level.php 0000644 00000012550 15107322020 0006317 0 ustar 00 <?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; use Psr\Log\LogLevel; /** * Represents the log levels * * Monolog supports the logging levels described by RFC 5424 {@see https://datatracker.ietf.org/doc/html/rfc5424} * but due to BC the severity values used internally are not 0-7. * * To get the level name/value out of a Level there are several options: * * - Use ->getName() to get the standard Monolog name which is full uppercased (e.g. "DEBUG") * - Use ->toPsrLogLevel() to get the standard PSR-3 name which is full lowercased (e.g. "debug") * - Use ->toRFC5424Level() to get the standard RFC 5424 value (e.g. 7 for debug, 0 for emergency) * - Use ->name to get the enum case's name which is capitalized (e.g. "Debug") * * To get the internal value for filtering, if the includes/isLowerThan/isHigherThan methods are * not enough, you can use ->value to get the enum case's integer value. */ enum Level: int { /** * Detailed debug information */ case Debug = 100; /** * Interesting events * * Examples: User logs in, SQL logs. */ case Info = 200; /** * Uncommon events */ case Notice = 250; /** * Exceptional occurrences that are not errors * * Examples: Use of deprecated APIs, poor use of an API, * undesirable things that are not necessarily wrong. */ case Warning = 300; /** * Runtime errors */ case Error = 400; /** * Critical conditions * * Example: Application component unavailable, unexpected exception. */ case Critical = 500; /** * Action must be taken immediately * * Example: Entire website down, database unavailable, etc. * This should trigger the SMS alerts and wake you up. */ case Alert = 550; /** * Urgent alert. */ case Emergency = 600; /** * @param value-of<self::NAMES>|LogLevel::*|'Debug'|'Info'|'Notice'|'Warning'|'Error'|'Critical'|'Alert'|'Emergency' $name * @return static */ public static function fromName(string $name): self { return match ($name) { 'debug', 'Debug', 'DEBUG' => self::Debug, 'info', 'Info', 'INFO' => self::Info, 'notice', 'Notice', 'NOTICE' => self::Notice, 'warning', 'Warning', 'WARNING' => self::Warning, 'error', 'Error', 'ERROR' => self::Error, 'critical', 'Critical', 'CRITICAL' => self::Critical, 'alert', 'Alert', 'ALERT' => self::Alert, 'emergency', 'Emergency', 'EMERGENCY' => self::Emergency, }; } /** * @param value-of<self::VALUES> $value * @return static */ public static function fromValue(int $value): self { return self::from($value); } /** * Returns true if the passed $level is higher or equal to $this */ public function includes(Level $level): bool { return $this->value <= $level->value; } public function isHigherThan(Level $level): bool { return $this->value > $level->value; } public function isLowerThan(Level $level): bool { return $this->value < $level->value; } /** * Returns the monolog standardized all-capitals name of the level * * Use this instead of $level->name which returns the enum case name (e.g. Debug vs DEBUG if you use getName()) * * @return value-of<self::NAMES> */ public function getName(): string { return match ($this) { self::Debug => 'DEBUG', self::Info => 'INFO', self::Notice => 'NOTICE', self::Warning => 'WARNING', self::Error => 'ERROR', self::Critical => 'CRITICAL', self::Alert => 'ALERT', self::Emergency => 'EMERGENCY', }; } /** * Returns the PSR-3 level matching this instance * * @phpstan-return \Psr\Log\LogLevel::* */ public function toPsrLogLevel(): string { return match ($this) { self::Debug => LogLevel::DEBUG, self::Info => LogLevel::INFO, self::Notice => LogLevel::NOTICE, self::Warning => LogLevel::WARNING, self::Error => LogLevel::ERROR, self::Critical => LogLevel::CRITICAL, self::Alert => LogLevel::ALERT, self::Emergency => LogLevel::EMERGENCY, }; } /** * Returns the RFC 5424 level matching this instance * * @phpstan-return int<0, 7> */ public function toRFC5424Level(): int { return match ($this) { self::Debug => 7, self::Info => 6, self::Notice => 5, self::Warning => 4, self::Error => 3, self::Critical => 2, self::Alert => 1, self::Emergency => 0, }; } public const VALUES = [ 100, 200, 250, 300, 400, 500, 550, 600, ]; public const NAMES = [ 'DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY', ]; } Test/TestCase.php 0000644 00000004646 15107322021 0007712 0 ustar 00 <?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\Test; use Monolog\Level; use Monolog\Logger; use Monolog\LogRecord; use Monolog\DateTimeImmutable; use Monolog\Formatter\FormatterInterface; use Psr\Log\LogLevel; /** * Lets you easily generate log records and a dummy formatter for testing purposes * * @author Jordi Boggiano <j.boggiano@seld.be> * * @internal feel free to reuse this to test your own handlers, this is marked internal to avoid issues with PHPStorm https://github.com/Seldaek/monolog/issues/1677 */ class TestCase extends \PHPUnit\Framework\TestCase { public function tearDown(): void { parent::tearDown(); if (isset($this->handler)) { unset($this->handler); } } /** * @param array<mixed> $context * @param array<mixed> $extra * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ protected function getRecord(int|string|Level $level = Level::Warning, string|\Stringable $message = 'test', array $context = [], string $channel = 'test', \DateTimeImmutable $datetime = new DateTimeImmutable(true), array $extra = []): LogRecord { return new LogRecord( message: (string) $message, context: $context, level: Logger::toMonologLevel($level), channel: $channel, datetime: $datetime, extra: $extra, ); } /** * @phpstan-return list<LogRecord> */ protected function getMultipleRecords(): array { return [ $this->getRecord(Level::Debug, 'debug message 1'), $this->getRecord(Level::Debug, 'debug message 2'), $this->getRecord(Level::Info, 'information'), $this->getRecord(Level::Warning, 'warning'), $this->getRecord(Level::Error, 'error'), ]; } protected function getIdentityFormatter(): FormatterInterface { $formatter = $this->createMock(FormatterInterface::class); $formatter->expects(self::any()) ->method('format') ->willReturnCallback(function ($record) { return $record->message; }); return $formatter; } } Handler/PsrHandler.php 0000644 00000004463 15107322021 0010674 0 ustar 00 <?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\Level; use Psr\Log\LoggerInterface; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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 */ protected LoggerInterface $logger; protected FormatterInterface|null $formatter = null; /** * @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied */ public function __construct(LoggerInterface $logger, int|string|Level $level = Level::Debug, bool $bubble = true) { parent::__construct($level, $bubble); $this->logger = $logger; } /** * @inheritDoc */ public function handle(LogRecord $record): bool { if (!$this->isHandling($record)) { return false; } if ($this->formatter !== null) { $formatted = $this->formatter->format($record); $this->logger->log($record->level->toPsrLogLevel(), (string) $formatted, $record->context); } else { $this->logger->log($record->level->toPsrLogLevel(), $record->message, $record->context); } return false === $this->bubble; } /** * Sets the formatter. */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $this->formatter = $formatter; return $this; } /** * Gets the formatter. */ public function getFormatter(): FormatterInterface { if ($this->formatter === null) { throw new \LogicException('No formatter has been set and this handler does not have a default formatter'); } return $this->formatter; } } Handler/ProcessHandler.php 0000644 00000012073 15107322021 0011542 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * 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; private string $command; private ?string $cwd; /** * @var resource[] */ private array $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, int|string|Level $level = Level::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(LogRecord $record): void { $this->ensureProcessIsStarted(); $this->writeProcessInput($record->formatted); $errors = $this->readProcessErrors(); if ($errors !== '') { 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 || $errors !== '') { 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; } } } Handler/RotatingFileHandler.php 0000644 00000015472 15107322021 0012521 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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'; protected string $filename; protected int $maxFiles; protected bool|null $mustRotate = null; protected \DateTimeImmutable $nextRotation; protected string $filenameFormat; protected string $dateFormat; /** * @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, int|string|Level $level = Level::Debug, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false, string $dateFormat = self::FILE_PER_DAY, string $filenameFormat = '{filename}-{date}') { $this->filename = Utils::canonicalizePath($filename); $this->maxFiles = $maxFiles; $this->setFilenameFormat($filenameFormat, $dateFormat); $this->nextRotation = $this->getNextRotation(); 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(): void { parent::reset(); if (true === $this->mustRotate) { $this->rotate(); } } /** * @return $this */ public function setFilenameFormat(string $filenameFormat, string $dateFormat): self { $this->setDateFormat($dateFormat); 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->url = $this->getTimedFilename(); $this->close(); return $this; } /** * @inheritDoc */ protected function write(LogRecord $record): void { // on the first record written, if the log is new, we should rotate (once per day) if (null === $this->mustRotate) { $this->mustRotate = null === $this->url || !file_exists($this->url); } if ($this->nextRotation <= $record->datetime) { $this->mustRotate = true; $this->close(); } parent::write($record); } /** * Rotates the files. */ protected function rotate(): void { // update filename $this->url = $this->getTimedFilename(); $this->nextRotation = $this->getNextRotation(); // 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(); } } $this->mustRotate = false; } 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; } protected function setDateFormat(string $dateFormat): void { if (0 === 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.' ); } $this->dateFormat = $dateFormat; } protected function getNextRotation(): \DateTimeImmutable { return match (str_replace(['/','_','.'], '-', $this->dateFormat)) { self::FILE_PER_MONTH => (new \DateTimeImmutable('first day of next month'))->setTime(0, 0, 0), self::FILE_PER_YEAR => (new \DateTimeImmutable('first day of January next year'))->setTime(0, 0, 0), default => (new \DateTimeImmutable('tomorrow'))->setTime(0, 0, 0), }; } } Handler/SqsHandler.php 0000644 00000003324 15107322022 0010672 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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; private SqsClient $client; private string $queueUrl; public function __construct(SqsClient $sqsClient, string $queueUrl, int|string|Level $level = Level::Debug, bool $bubble = true) { parent::__construct($level, $bubble); $this->client = $sqsClient; $this->queueUrl = $queueUrl; } /** * @inheritDoc */ protected function write(LogRecord $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, ]); } } Handler/InsightOpsHandler.php 0000644 00000004047 15107322022 0012216 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * Inspired on LogEntriesHandler. * * @author Robert Kaufmann III <rok3@rok3.me> * @author Gabriel Machado <gabriel.ms1@hotmail.com> */ class InsightOpsHandler extends SocketHandler { protected string $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 = Level::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(LogRecord $record): string { return $this->logToken . ' ' . $record->formatted; } } Handler/Curl/Util.php 0000644 00000003402 15107322022 0010445 0 ustar 00 <?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 array $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 CurlHandle $ch curl handler * @return bool|string @see curl_exec */ public static function execute(CurlHandle $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 === 0) { $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; } } Handler/LogEntriesHandler.php 0000644 00000003562 15107322022 0012203 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * @author Robert Kaufmann III <rok3@rok3.me> */ class LogEntriesHandler extends SocketHandler { protected string $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 = Level::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(LogRecord $record): string { return $this->logToken . ' ' . $record->formatted; } } Handler/HandlerInterface.php 0000644 00000005342 15107322022 0012026 0 ustar 00 <?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\LogRecord; /** * Interface that all Monolog Handlers must implement * * @author Jordi Boggiano <j.boggiano@seld.be> */ 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 LogRecord $record Partial log record having only a level initialized */ public function isHandling(LogRecord $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 LogRecord $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. */ public function handle(LogRecord $record): bool; /** * Handles a set of records at once. * * @param array<LogRecord> $records The records to handle */ 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; } Handler/FlowdockHandler.php 0000644 00000006630 15107322022 0011677 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\Formatter\FlowdockFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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 * @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 { protected string $apiToken; /** * @throws MissingExtensionException if OpenSSL is missing */ public function __construct( string $apiToken, $level = Level::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(LogRecord $record): void { parent::write($record); $this->closeSocket(); } /** * @inheritDoc */ protected function generateDataStream(LogRecord $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } /** * Builds the body of API call */ private function buildContent(LogRecord $record): string { return Utils::jsonEncode($record->formatted); } /** * 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; } } Handler/ErrorLogHandler.php 0000644 00000005164 15107322023 0011664 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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; protected int $messageType; protected bool $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 * * @throws \InvalidArgumentException If an unsupported message type is set */ public function __construct(int $messageType = self::OPERATING_SYSTEM, int|string|Level $level = Level::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(LogRecord $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); } } } Handler/AbstractHandler.php 0000644 00000005167 15107322023 0011677 0 ustar 00 <?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\Level; use Monolog\Logger; use Monolog\ResettableInterface; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * Base Handler class providing basic level/bubble support * * @author Jordi Boggiano <j.boggiano@seld.be> */ abstract class AbstractHandler extends Handler implements ResettableInterface { protected Level $level = Level::Debug; protected bool $bubble = true; /** * @param int|string|Level|LogLevel::* $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 value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function __construct(int|string|Level $level = Level::Debug, bool $bubble = true) { $this->setLevel($level); $this->bubble = $bubble; } /** * @inheritDoc */ public function isHandling(LogRecord $record): bool { return $record->level->value >= $this->level->value; } /** * Sets minimum logging level at which this handler will be triggered. * * @param Level|LogLevel::* $level Level or level name * @return $this * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function setLevel(int|string|Level $level): self { $this->level = Logger::toMonologLevel($level); return $this; } /** * Gets minimum logging level at which this handler will be triggered. */ public function getLevel(): Level { 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 $this */ 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(): void { } } Handler/ProcessableHandlerInterface.php 0000644 00000002241 15107322023 0014205 0 ustar 00 <?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; use Monolog\LogRecord; /** * Interface to describe loggers that have processors * * @author Jordi Boggiano <j.boggiano@seld.be> */ interface ProcessableHandlerInterface { /** * Adds a processor in the stack. * * @phpstan-param ProcessorInterface|(callable(LogRecord): LogRecord) $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. * * @phpstan-return ProcessorInterface|(callable(LogRecord): LogRecord) $callback * * @throws \LogicException In case the processor stack is empty * @return callable|ProcessorInterface */ public function popProcessor(): callable; } Handler/OverflowHandler.php 0000644 00000010325 15107322023 0011727 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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, [Level::Warning->value => 10, Level::Error->value => 5]); * * $log->pushHandler($overflow); *``` * * @author Kris Buist <krisbuist@gmail.com> */ class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface { private HandlerInterface $handler; /** @var array<int, int> */ private array $thresholdMap = []; /** * Buffer of all messages passed to the handler before the threshold was reached * * @var mixed[][] */ private array $buffer = []; /** * @param array<int, int> $thresholdMap Dictionary of log level value => threshold */ public function __construct( HandlerInterface $handler, array $thresholdMap = [], $level = Level::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(LogRecord $record): bool { if ($record->level->isLowerThan($this->level)) { return false; } $level = $record->level->value; 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.'); } } Handler/SlackHandler.php 0000644 00000015763 15107322024 0011175 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\Handler\Slack\SlackRecord; use Monolog\LogRecord; /** * Sends notifications through Slack API * * @author Greg Kedzierski <greg@gregkedzierski.com> * @see https://api.slack.com/ */ class SlackHandler extends SocketHandler { /** * Slack API token */ private string $token; /** * Instance of the SlackRecord util class preparing data for Slack API. */ private SlackRecord $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 = Level::Critical, bool $bubble = true, bool $useShortAttachment = false, bool $includeContextAndExtra = false, array $excludeFields = [], 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(LogRecord $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } /** * Builds the body of API call */ private function buildContent(LogRecord $record): string { $dataArray = $this->prepareContentData($record); return http_build_query($dataArray); } /** * @return string[] */ protected function prepareContentData(LogRecord $record): array { $dataArray = $this->slackRecord->getSlackData($record); $dataArray['token'] = $this->token; if (isset($dataArray['attachments']) && is_array($dataArray['attachments']) && \count($dataArray['attachments']) > 0) { $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(LogRecord $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 * * @return $this */ public function setChannel(string $channel): self { $this->slackRecord->setChannel($channel); return $this; } /** * Username used by the bot when posting * * @return $this */ public function setUsername(string $username): self { $this->slackRecord->setUsername($username); return $this; } /** * @return $this */ public function useAttachment(bool $useAttachment): self { $this->slackRecord->useAttachment($useAttachment); return $this; } /** * @return $this */ public function setIconEmoji(string $iconEmoji): self { $this->slackRecord->setUserIcon($iconEmoji); return $this; } /** * @return $this */ public function useShortAttachment(bool $useShortAttachment): self { $this->slackRecord->useShortAttachment($useShortAttachment); return $this; } /** * @return $this */ public function includeContextAndExtra(bool $includeContextAndExtra): self { $this->slackRecord->includeContextAndExtra($includeContextAndExtra); return $this; } /** * @param string[] $excludeFields * @return $this */ public function excludeFields(array $excludeFields): self { $this->slackRecord->excludeFields($excludeFields); return $this; } } Handler/MandrillHandler.php 0000644 00000004746 15107322024 0011701 0 ustar 00 <?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\Level; 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 { protected Swift_Message $message; protected string $apiKey; /** * @phpstan-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 * * @throws \InvalidArgumentException if not a Swift Message is set */ public function __construct(string $apiKey, callable|Swift_Message $message, int|string|Level $level = Level::Error, bool $bubble = true) { parent::__construct($level, $bubble); if (!$message instanceof Swift_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); } } Handler/HandlerWrapper.php 0000644 00000006433 15107322024 0011552 0 ustar 00 <?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; use Monolog\LogRecord; /** * 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(LogRecord $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 { protected HandlerInterface $handler; public function __construct(HandlerInterface $handler) { $this->handler = $handler; } /** * @inheritDoc */ public function isHandling(LogRecord $record): bool { return $this->handler->isHandling($record); } /** * @inheritDoc */ public function handle(LogRecord $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(): void { if ($this->handler instanceof ResettableInterface) { $this->handler->reset(); } } } Handler/PHPConsoleHandler.php 0000644 00000027661 15107322024 0012112 0 ustar 00 <?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\Level; use Monolog\Utils; use PhpConsole\Connector; use PhpConsole\Handler as VendorPhpConsoleHandler; use PhpConsole\Helper; use Monolog\LogRecord; use PhpConsole\Storage; /** * 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-type Options array{ * enabled: bool, * classesPartialsTraceIgnore: string[], * debugTagsKeysInContext: array<int|string>, * useOwnErrorsHandler: bool, * useOwnExceptionsHandler: bool, * sourcesBasePath: string|null, * registerHelper: bool, * serverEncoding: string|null, * headersLimit: int|null, * password: string|null, * enableSslOnlyMode: bool, * ipMasks: string[], * enableEvalListener: bool, * dumperDetectCallbacks: bool, * dumperLevelLimit: int, * dumperItemsCountLimit: int, * dumperItemSizeLimit: int, * dumperDumpSizeLimit: int, * detectDumpTraceAndSource: bool, * dataStorage: Storage|null * } * @phpstan-type InputOptions array{ * enabled?: bool, * classesPartialsTraceIgnore?: string[], * debugTagsKeysInContext?: array<int|string>, * useOwnErrorsHandler?: bool, * useOwnExceptionsHandler?: bool, * sourcesBasePath?: string|null, * registerHelper?: bool, * serverEncoding?: string|null, * headersLimit?: int|null, * password?: string|null, * enableSslOnlyMode?: bool, * ipMasks?: string[], * enableEvalListener?: bool, * dumperDetectCallbacks?: bool, * dumperLevelLimit?: int, * dumperItemsCountLimit?: int, * dumperItemSizeLimit?: int, * dumperDumpSizeLimit?: int, * detectDumpTraceAndSource?: bool, * dataStorage?: Storage|null * } * * @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 { /** * @phpstan-var Options */ private array $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) ]; private Connector $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 * @phpstan-param InputOptions $options */ public function __construct(array $options = [], ?Connector $connector = null, int|string|Level $level = Level::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> * * @phpstan-param InputOptions $options * @phpstan-return Options */ private function initOptions(array $options): array { $wrongOptions = array_diff(array_keys($options), array_keys($this->options)); if (\count($wrongOptions) > 0) { throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions)); } return array_replace($this->options, $options); } private function initConnector(?Connector $connector = null): Connector { if (null === $connector) { if ($this->options['dataStorage'] instanceof Storage) { 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 (null !== $this->options['sourcesBasePath']) { $connector->setSourcesBasePath($this->options['sourcesBasePath']); } if (null !== $this->options['serverEncoding']) { $connector->setServerEncoding($this->options['serverEncoding']); } if (null !== $this->options['password']) { $connector->setPassword($this->options['password']); } if ($this->options['enableSslOnlyMode']) { $connector->enableSslOnlyMode(); } if (\count($this->options['ipMasks']) > 0) { $connector->setAllowedIpMasks($this->options['ipMasks']); } if (null !== $this->options['headersLimit'] && $this->options['headersLimit'] > 0) { $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(LogRecord $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(LogRecord $record): void { if ($record->level->isLowerThan(Level::Notice)) { $this->handleDebugRecord($record); } elseif (isset($record->context['exception']) && $record->context['exception'] instanceof \Throwable) { $this->handleExceptionRecord($record); } else { $this->handleErrorRecord($record); } } private function handleDebugRecord(LogRecord $record): void { [$tags, $filteredContext] = $this->getRecordTags($record); $message = $record->message; if (\count($filteredContext) > 0) { $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($filteredContext)), null, true); } $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']); } private function handleExceptionRecord(LogRecord $record): void { $this->connector->getErrorsDispatcher()->dispatchException($record->context['exception']); } private function handleErrorRecord(LogRecord $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'] ); } /** * @return array{string, mixed[]} */ private function getRecordTags(LogRecord $record): array { $tags = null; $filteredContext = []; if ($record->context !== []) { $filteredContext = $record->context; foreach ($this->options['debugTagsKeysInContext'] as $key) { if (isset($filteredContext[$key])) { $tags = $filteredContext[$key]; if ($key === 0) { array_shift($filteredContext); } else { unset($filteredContext[$key]); } break; } } } return [$tags ?? $record->level->toPsrLogLevel(), $filteredContext]; } /** * @inheritDoc */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter('%message%'); } } Handler/Handler.php 0000644 00000002333 15107322025 0010205 0 ustar 00 <?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; } } Handler/Slack/SlackRecord.php 0000644 00000024553 15107322025 0012071 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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 */ 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) */ private string|null $channel; /** * Name of a bot */ private string|null $username; /** * User icon e.g. 'ghost', 'http://example.com/user.png' */ private string|null $userIcon; /** * Whether the message should be added to Slack as attachment (plain text otherwise) */ private bool $useAttachment; /** * Whether the the context/extra messages added to Slack as attachments are in a short style */ private bool $useShortAttachment; /** * Whether the attachment should include context and extra data */ private bool $includeContextAndExtra; /** * Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] * @var string[] */ private array $excludeFields; private FormatterInterface|null $formatter; private NormalizerFormatter $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 = [], 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-return mixed[] */ public function getSlackData(LogRecord $record): array { $dataArray = []; if ($this->username !== null) { $dataArray['username'] = $this->username; } if ($this->channel !== null) { $dataArray['channel'] = $this->channel; } if ($this->formatter !== null && !$this->useAttachment) { $message = $this->formatter->format($record); } else { $message = $record->message; } $recordData = $this->removeExcludedFields($record); if ($this->useAttachment) { $attachment = [ 'fallback' => $message, 'text' => $message, 'color' => $this->getAttachmentColor($record->level), 'fields' => [], 'mrkdwn_in' => ['fields'], 'ts' => $recordData['datetime']->getTimestamp(), 'footer' => $this->username, 'footer_icon' => $this->userIcon, ]; if ($this->useShortAttachment) { $attachment['title'] = $recordData['level_name']; } else { $attachment['title'] = 'Message'; $attachment['fields'][] = $this->generateAttachmentField('Level', $recordData['level_name']); } if ($this->includeContextAndExtra) { foreach (['extra', 'context'] as $key) { if (!isset($recordData[$key]) || \count($recordData[$key]) === 0) { continue; } if ($this->useShortAttachment) { $attachment['fields'][] = $this->generateAttachmentField( $key, $recordData[$key] ); } else { // Add all extra fields as individual fields in attachment $attachment['fields'] = array_merge( $attachment['fields'], $this->generateAttachmentFields($recordData[$key]) ); } } } $dataArray['attachments'] = [$attachment]; } else { $dataArray['text'] = $message; } if ($this->userIcon !== null) { if (false !== ($iconUrl = filter_var($this->userIcon, FILTER_VALIDATE_URL))) { $dataArray['icon_url'] = $iconUrl; } else { $dataArray['icon_emoji'] = ":{$this->userIcon}:"; } } return $dataArray; } /** * Returns a Slack message attachment color associated with * provided level. */ public function getAttachmentColor(Level $level): string { return match ($level) { Level::Error, Level::Critical, Level::Alert, Level::Emergency => static::COLOR_DANGER, Level::Warning => static::COLOR_WARNING, Level::Info, Level::Notice => static::COLOR_GOOD, Level::Debug => 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 array<mixed> $normalized */ $normalized = $this->normalizerFormatter->normalizeValue($fields); $hasSecondDimension = \count(array_filter($normalized, 'is_array')) > 0; $hasOnlyNonNumericKeys = \count(array_filter(array_keys($normalized), 'is_numeric')) === 0; return $hasSecondDimension || $hasOnlyNonNumericKeys ? 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 $this */ public function setChannel(?string $channel = null): self { $this->channel = $channel; return $this; } /** * Username used by the bot when posting * * @param ?string $username * @return $this */ public function setUsername(?string $username = null): self { $this->username = $username; return $this; } /** * @return $this */ public function useAttachment(bool $useAttachment = true): self { $this->useAttachment = $useAttachment; return $this; } /** * @return $this */ public function setUserIcon(?string $userIcon = null): self { $this->userIcon = $userIcon; if (\is_string($userIcon)) { $this->userIcon = trim($userIcon, ':'); } return $this; } /** * @return $this */ public function useShortAttachment(bool $useShortAttachment = false): self { $this->useShortAttachment = $useShortAttachment; return $this; } /** * @return $this */ public function includeContextAndExtra(bool $includeContextAndExtra = false): self { $this->includeContextAndExtra = $includeContextAndExtra; if ($this->includeContextAndExtra) { $this->normalizerFormatter = new NormalizerFormatter(); } return $this; } /** * @param string[] $excludeFields * @return $this */ public function excludeFields(array $excludeFields = []): self { $this->excludeFields = $excludeFields; return $this; } /** * @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 [ '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 array<mixed> $normalized */ $normalized = $this->normalizerFormatter->normalizeValue($data); $fields = []; 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 * * @return mixed[] */ private function removeExcludedFields(LogRecord $record): array { $recordData = $record->toArray(); foreach ($this->excludeFields as $field) { $keys = explode('.', $field); $node = &$recordData; $lastKey = end($keys); foreach ($keys as $key) { if (!isset($node[$key])) { break; } if ($lastKey === $key) { unset($node[$key]); break; } $node = &$node[$key]; } } return $recordData; } } Handler/FallbackGroupHandler.php 0000644 00000003270 15107322025 0012643 0 ustar 00 <?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; use Monolog\LogRecord; /** * 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. */ class FallbackGroupHandler extends GroupHandler { /** * @inheritDoc */ public function handle(LogRecord $record): bool { if (\count($this->processors) > 0) { $record = $this->processRecord($record); } foreach ($this->handlers as $handler) { try { $handler->handle(clone $record); break; } catch (Throwable $e) { // What throwable? } } return false === $this->bubble; } /** * @inheritDoc */ public function handleBatch(array $records): void { if (\count($this->processors) > 0) { $processed = []; foreach ($records as $record) { $processed[] = $this->processRecord($record); } $records = $processed; } foreach ($this->handlers as $handler) { try { $handler->handleBatch(array_map(fn ($record) => clone $record, $records)); break; } catch (Throwable $e) { // What throwable? } } } } Handler/ChromePHPHandler.php 0000644 00000012021 15107322026 0011707 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; use Monolog\DateTimeImmutable; /** * 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> */ 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}'; protected static bool $initialized = false; /** * Tracks whether we sent too much data * * Chrome limits the headers to 4KB, so when we sent 3KB we stop sending */ protected static bool $overflowed = false; /** @var mixed[] */ protected static array $json = [ 'version' => self::VERSION, 'columns' => ['label', 'log', 'backtrace', 'type'], 'rows' => [], ]; protected static bool $sendHeaders = true; /** * @throws \RuntimeException If the function json_encode does not exist */ public function __construct(int|string|Level $level = Level::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; } $message = $this->processRecord($record); $messages[] = $message; } if (\count($messages) > 0) { $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(LogRecord $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 = new LogRecord( message: 'Incomplete logs, chrome header size limit reached', level: Level::Warning, channel: 'monolog', datetime: new DateTimeImmutable(true), ); 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 (!isset($_SERVER['HTTP_USER_AGENT'])) { return false; } return preg_match(static::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']) === 1; } } Handler/DoctrineCouchDBHandler.php 0000644 00000002176 15107322026 0013073 0 ustar 00 <?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\Level; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; use Doctrine\CouchDB\CouchDBClient; use Monolog\LogRecord; /** * CouchDB handler for Doctrine CouchDB ODM * * @author Markus Bachmann <markus.bachmann@bachi.biz> */ class DoctrineCouchDBHandler extends AbstractProcessingHandler { private CouchDBClient $client; public function __construct(CouchDBClient $client, int|string|Level $level = Level::Debug, bool $bubble = true) { $this->client = $client; parent::__construct($level, $bubble); } /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->client->postDocument($record->formatted); } protected function getDefaultFormatter(): FormatterInterface { return new NormalizerFormatter; } } Handler/WhatFailureGroupHandler.php 0000644 00000003567 15107322026 0013371 0 ustar 00 <?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\LogRecord; use Throwable; /** * 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> */ class WhatFailureGroupHandler extends GroupHandler { /** * @inheritDoc */ public function handle(LogRecord $record): bool { if (\count($this->processors) > 0) { $record = $this->processRecord($record); } foreach ($this->handlers as $handler) { try { $handler->handle(clone $record); } catch (Throwable) { // What failure? } } return false === $this->bubble; } /** * @inheritDoc */ public function handleBatch(array $records): void { if (\count($this->processors) > 0) { $processed = []; foreach ($records as $record) { $processed[] = $this->processRecord($record); } $records = $processed; } foreach ($this->handlers as $handler) { try { $handler->handleBatch(array_map(fn ($record) => clone $record, $records)); } catch (Throwable) { // What failure? } } } /** * {@inheritDoc} */ public function close(): void { foreach ($this->handlers as $handler) { try { $handler->close(); } catch (\Throwable $e) { // What failure? } } } } Handler/DynamoDbHandler.php 0000644 00000003616 15107322026 0011631 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * 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'; protected DynamoDbClient $client; protected string $table; protected Marshaler $marshaler; public function __construct(DynamoDbClient $client, string $table, int|string|Level $level = Level::Debug, bool $bubble = true) { $this->marshaler = new Marshaler; $this->client = $client; $this->table = $table; parent::__construct($level, $bubble); } /** * @inheritDoc */ protected function write(LogRecord $record): void { $filtered = $this->filterEmptyFields($record->formatted); $formatted = $this->marshaler->marshalItem($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 [] !== $value; }); } /** * @inheritDoc */ protected function getDefaultFormatter(): FormatterInterface { return new ScalarFormatter(self::DATE_FORMAT); } } Handler/NoopHandler.php 0000644 00000001614 15107322027 0011044 0 ustar 00 <?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\LogRecord; /** * 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(LogRecord $record): bool { return true; } /** * @inheritDoc */ public function handle(LogRecord $record): bool { return false; } } Handler/FingersCrossed/ChannelLevelActivationStrategy.php 0000644 00000004606 15107322027 0017664 0 ustar 00 <?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\Level; use Monolog\Logger; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * 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( * Level::Critical, * array( * 'request' => Level::Alert, * 'sensitive' => Level::Error, * ) * ); * $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy); * </code> * * @author Mike Meessen <netmikey@gmail.com> */ class ChannelLevelActivationStrategy implements ActivationStrategyInterface { private Level $defaultActionLevel; /** * @var array<string, Level> */ private array $channelToActionLevel; /** * @param int|string|Level|LogLevel::* $defaultActionLevel The default action level to be used if the record's category doesn't match any * @param array<string, int|string|Level|LogLevel::*> $channelToActionLevel An array that maps channel names to action levels. * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $defaultActionLevel * @phpstan-param array<string, value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::*> $channelToActionLevel */ public function __construct(int|string|Level $defaultActionLevel, array $channelToActionLevel = []) { $this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel); $this->channelToActionLevel = array_map(Logger::toMonologLevel(...), $channelToActionLevel); } public function isHandlerActivated(LogRecord $record): bool { if (isset($this->channelToActionLevel[$record->channel])) { return $record->level->value >= $this->channelToActionLevel[$record->channel]->value; } return $record->level->value >= $this->defaultActionLevel->value; } } Handler/FingersCrossed/ErrorLevelActivationStrategy.php 0000644 00000002073 15107322027 0017401 0 ustar 00 <?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\Level; use Monolog\LogRecord; use Monolog\Logger; use Psr\Log\LogLevel; /** * Error level based activation strategy. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ class ErrorLevelActivationStrategy implements ActivationStrategyInterface { private Level $actionLevel; /** * @param int|string|Level $actionLevel Level or name or value * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $actionLevel */ public function __construct(int|string|Level $actionLevel) { $this->actionLevel = Logger::toMonologLevel($actionLevel); } public function isHandlerActivated(LogRecord $record): bool { return $record->level->value >= $this->actionLevel->value; } } Handler/FingersCrossed/ActivationStrategyInterface.php 0000644 00000001211 15107322027 0017211 0 ustar 00 <?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\LogRecord; /** * Interface for activation strategies for the FingersCrossedHandler. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ interface ActivationStrategyInterface { /** * Returns whether the given record activates the handler. */ public function isHandlerActivated(LogRecord $record): bool; } Handler/NativeMailerHandler.php 0000644 00000011605 15107322027 0012512 0 ustar 00 <?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\Level; 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 array $to; /** * The subject of the email */ protected string $subject; /** * Optional headers for the message * @var string[] */ protected array $headers = []; /** * Optional parameters for the message * @var string[] */ protected array $parameters = []; /** * The wordwrap length for the message */ protected int $maxColumnWidth; /** * The Content-type for the message */ protected string|null $contentType = null; /** * The encoding for the message */ protected string $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(string|array $to, string $subject, string $from, int|string|Level $level = Level::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 * @return $this */ 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 * @return $this */ 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"; } $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. * @return $this */ 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; } /** * @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; } } Handler/AbstractProcessingHandler.php 0000644 00000002733 15107322030 0013726 0 ustar 00 <?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\LogRecord; /** * 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> */ abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; use FormattableHandlerTrait; /** * @inheritDoc */ public function handle(LogRecord $record): bool { if (!$this->isHandling($record)) { return false; } if (\count($this->processors) > 0) { $record = $this->processRecord($record); } $record->formatted = $this->getFormatter()->format($record); $this->write($record); return false === $this->bubble; } /** * Writes the (already formatted) record down to the log of the implementing handler */ abstract protected function write(LogRecord $record): void; public function reset(): void { parent::reset(); $this->resetProcessors(); } } Handler/DeduplicationHandler.php 0000644 00000013466 15107322030 0012717 0 ustar 00 <?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\Level; use Monolog\Logger; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * 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> */ class DeduplicationHandler extends BufferHandler { protected string $deduplicationStore; protected Level $deduplicationLevel; protected int $time; private bool $gc = false; /** * @param HandlerInterface $handler Handler. * @param string|null $deduplicationStore The file/path where the deduplication log should be kept * @param int|string|Level|LogLevel::* $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 value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $deduplicationLevel */ public function __construct(HandlerInterface $handler, ?string $deduplicationStore = null, int|string|Level $deduplicationLevel = Level::Error, int $time = 60, bool $bubble = true) { parent::__construct($handler, 0, Level::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->value >= $this->deduplicationLevel->value) { $passthru = $passthru === true || !$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(); } } private function isDuplicate(LogRecord $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->getName() && $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 (false === $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 (is_string($log) && '' !== $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; } private function appendRecord(LogRecord $record): void { file_put_contents($this->deduplicationStore, $record->datetime->getTimestamp() . ':' . $record->level->getName() . ':' . preg_replace('{[\r\n].*}', '', $record->message) . "\n", FILE_APPEND); } } Handler/MailHandler.php 0000644 00000004305 15107322030 0011005 0 ustar 00 <?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; use Monolog\LogRecord; /** * Base class for all mail handlers * * @author Gyula Sallai */ abstract class MailHandler extends AbstractProcessingHandler { /** * @inheritDoc */ public function handleBatch(array $records): void { $messages = []; foreach ($records as $record) { if ($record->level->isLowerThan($this->level)) { continue; } $message = $this->processRecord($record); $messages[] = $message; } if (\count($messages) > 0) { $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 non-empty-array<LogRecord> $records */ abstract protected function send(string $content, array $records): void; /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->send((string) $record->formatted, [$record]); } /** * @phpstan-param non-empty-array<LogRecord> $records */ protected function getHighestRecord(array $records): LogRecord { $highestRecord = null; foreach ($records as $record) { if ($highestRecord === null || $record->level->isHigherThan($highestRecord->level)) { $highestRecord = $record; } } return $highestRecord; } protected function isHtmlBody(string $body): bool { return ($body[0] ?? null) === '<'; } /** * Gets the default formatter. */ protected function getDefaultFormatter(): FormatterInterface { return new HtmlFormatter(); } } Handler/FormattableHandlerTrait.php 0000644 00000002364 15107322030 0013372 0 ustar 00 <?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 { protected FormatterInterface|null $formatter = null; /** * @inheritDoc */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $this->formatter = $formatter; return $this; } /** * @inheritDoc */ public function getFormatter(): FormatterInterface { if (null === $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(); } } Handler/ElasticsearchHandler.php 0000644 00000015451 15107322031 0012702 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\ElasticsearchFormatter; use InvalidArgumentException; use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException; use Elasticsearch\Client; use Monolog\LogRecord; 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> * @phpstan-type Options array{ * index: string, * type: string, * ignore_error: bool, * op_type: 'index'|'create' * } * @phpstan-type InputOptions array{ * index?: string, * type?: string, * ignore_error?: bool, * op_type?: 'index'|'create' * } */ class ElasticsearchHandler extends AbstractProcessingHandler { protected Client|Client8 $client; /** * @var mixed[] Handler config options * @phpstan-var Options */ protected array $options; /** * @var bool */ private $needsType; /** * @param Client|Client8 $client Elasticsearch Client object * @param mixed[] $options Handler configuration * * @phpstan-param InputOptions $options */ public function __construct(Client|Client8 $client, array $options = [], int|string|Level $level = Level::Debug, bool $bubble = true) { 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 'op_type' => 'index', // Elastic op_type (index or create) (https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#docs-index-api-op_type) ], $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(LogRecord $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[] * * @phpstan-return Options */ 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<array<mixed>> $records Records + _index/_type keys * @throws \RuntimeException */ protected function bulkSend(array $records): void { try { $params = [ 'body' => [], ]; foreach ($records as $record) { $params['body'][] = [ $this->options['op_type'] => $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 { 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); } } Handler/RedisPubSubHandler.php 0000644 00000003223 15107322031 0012311 0 ustar 00 <?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\Level; use Monolog\LogRecord; use Predis\Client as Predis; use Redis; /** * 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", Level::Warning); * $log->pushHandler($redis); * * @author Gaëtan Faugère <gaetan@fauge.re> */ class RedisPubSubHandler extends AbstractProcessingHandler { /** @var Predis<Predis>|Redis */ private Predis|Redis $redisClient; private string $channelKey; /** * @param Predis<Predis>|Redis $redis The redis instance * @param string $key The channel key to publish records to */ public function __construct(Predis|Redis $redis, string $key, int|string|Level $level = Level::Debug, bool $bubble = true) { $this->redisClient = $redis; $this->channelKey = $key; parent::__construct($level, $bubble); } /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->redisClient->publish($this->channelKey, $record->formatted); } /** * @inheritDoc */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter(); } } Handler/SymfonyMailerHandler.php 0000644 00000006735 15107322031 0012733 0 ustar 00 <?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 Closure; use Monolog\Level; use Monolog\Logger; use Monolog\LogRecord; 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> */ class SymfonyMailerHandler extends MailHandler { protected MailerInterface|TransportInterface $mailer; /** @var Email|Closure(string, LogRecord[]): Email */ private Email|Closure $emailTemplate; /** * @phpstan-param Email|Closure(string, LogRecord[]): Email $email * * @param MailerInterface|TransportInterface $mailer The mailer to use * @param Closure|Email $email An email template, the subject/body will be replaced */ public function __construct($mailer, Email|Closure $email, int|string|Level $level = Level::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 LogRecord[] $records Log records that formed the content */ 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 instanceof LogRecord ? Utils::getRecordMessageForException($record) : '')); } if (\count($records) > 0) { $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()); } } Handler/SyslogUdp/UdpSocket.php 0000644 00000003653 15107322032 0012466 0 ustar 00 <?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; protected string $ip; protected int $port; protected ?Socket $socket = null; public function __construct(string $ip, int $port = 514) { $this->ip = $ip; $this->port = $port; } public function write(string $line, string $header = ""): void { $this->send($this->assembleMessage($line, $header)); } public function close(): void { if ($this->socket instanceof Socket) { socket_close($this->socket); $this->socket = null; } } protected function getSocket(): Socket { 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; } $socket = socket_create($domain, SOCK_DGRAM, $protocol); if ($socket instanceof Socket) { return $this->socket = $socket; } throw new \RuntimeException('The UdpSocket to '.$this->ip.':'.$this->port.' could not be opened via socket_create'); } 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); } } Handler/MongoDBHandler.php 0000644 00000004474 15107322032 0011421 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\MongoDBFormatter; use Monolog\LogRecord; /** * 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 { private \MongoDB\Collection $collection; private Client|Manager $manager; private string|null $namespace = null; /** * Constructor. * * @param Client|Manager $mongodb MongoDB library or driver client * @param string $database Database name * @param string $collection Collection name */ public function __construct(Client|Manager $mongodb, string $database, string $collection, int|string|Level $level = Level::Debug, bool $bubble = true) { 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(LogRecord $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; } } Handler/SamplingHandler.php 0000644 00000007506 15107322032 0011705 0 ustar 00 <?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 Closure; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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> */ class SamplingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** * Handler or factory Closure($record, $this) * * @phpstan-var (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface */ protected Closure|HandlerInterface $handler; protected int $factor; /** * @phpstan-param (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface $handler * * @param Closure|HandlerInterface $handler Handler or factory Closure($record|null, $samplingHandler). * @param int $factor Sample factor (e.g. 10 means every ~10th record is sampled) */ public function __construct(Closure|HandlerInterface $handler, int $factor) { parent::__construct(); $this->handler = $handler; $this->factor = $factor; } public function isHandling(LogRecord $record): bool { return $this->getHandler($record)->isHandling($record); } public function handle(LogRecord $record): bool { if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) { if (\count($this->processors) > 0) { $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, this will trigger the handler's instantiation. */ public function getHandler(LogRecord $record = null): HandlerInterface { if (!$this->handler instanceof HandlerInterface) { $handler = ($this->handler)($record, $this); if (!$handler instanceof HandlerInterface) { throw new \RuntimeException("The factory Closure should return a HandlerInterface"); } $this->handler = $handler; } 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.'); } } Handler/FleepHookHandler.php 0000644 00000006703 15107322032 0012005 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * 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> */ 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 string $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 if OpenSSL is missing */ public function __construct( string $token, $level = Level::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(LogRecord $record): void { parent::write($record); $this->closeSocket(); } /** * @inheritDoc */ protected function generateDataStream(LogRecord $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 */ private function buildContent(LogRecord $record): string { $dataArray = [ 'message' => $record->formatted, ]; return http_build_query($dataArray); } } Handler/IFTTTHandler.php 0000644 00000004334 15107322033 0011022 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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 { private string $eventName; private string $secretKey; /** * @param string $eventName The name of the IFTTT Maker event that should be triggered * @param string $secretKey A valid IFTTT secret key * * @throws MissingExtensionException If the curl extension is missing */ public function __construct(string $eventName, string $secretKey, int|string|Level $level = Level::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(LogRecord $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); } } Handler/TestHandler.php 0000644 00000014476 15107322033 0011057 0 ustar 00 <?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\Level; use Monolog\Logger; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * 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(string|array $recordAssertions) * @method bool hasAlert(string|array $recordAssertions) * @method bool hasCritical(string|array $recordAssertions) * @method bool hasError(string|array $recordAssertions) * @method bool hasWarning(string|array $recordAssertions) * @method bool hasNotice(string|array $recordAssertions) * @method bool hasInfo(string|array $recordAssertions) * @method bool hasDebug(string|array $recordAssertions) * * @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(string $message) * @method bool hasAlertThatContains(string $message) * @method bool hasCriticalThatContains(string $message) * @method bool hasErrorThatContains(string $message) * @method bool hasWarningThatContains(string $message) * @method bool hasNoticeThatContains(string $message) * @method bool hasInfoThatContains(string $message) * @method bool hasDebugThatContains(string $message) * * @method bool hasEmergencyThatMatches(string $regex) * @method bool hasAlertThatMatches(string $regex) * @method bool hasCriticalThatMatches(string $regex) * @method bool hasErrorThatMatches(string $regex) * @method bool hasWarningThatMatches(string $regex) * @method bool hasNoticeThatMatches(string $regex) * @method bool hasInfoThatMatches(string $regex) * @method bool hasDebugThatMatches(string $regex) * * @method bool hasEmergencyThatPasses(callable $predicate) * @method bool hasAlertThatPasses(callable $predicate) * @method bool hasCriticalThatPasses(callable $predicate) * @method bool hasErrorThatPasses(callable $predicate) * @method bool hasWarningThatPasses(callable $predicate) * @method bool hasNoticeThatPasses(callable $predicate) * @method bool hasInfoThatPasses(callable $predicate) * @method bool hasDebugThatPasses(callable $predicate) */ class TestHandler extends AbstractProcessingHandler { /** @var LogRecord[] */ protected array $records = []; /** @phpstan-var array<value-of<Level::VALUES>, LogRecord[]> */ protected array $recordsByLevel = []; private bool $skipReset = false; /** * @return array<LogRecord> */ public function getRecords(): array { return $this->records; } public function clear(): void { $this->records = []; $this->recordsByLevel = []; } public function reset(): void { if (!$this->skipReset) { $this->clear(); } } public function setSkipReset(bool $skipReset): void { $this->skipReset = $skipReset; } /** * @param int|string|Level|LogLevel::* $level Logging level value or name * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function hasRecords(int|string|Level $level): bool { return isset($this->recordsByLevel[Logger::toMonologLevel($level)->value]); } /** * @param string|array $recordAssertions Either a message string or an array containing message and optionally context keys that will be checked against all records * * @phpstan-param array{message: string, context?: mixed[]}|string $recordAssertions */ public function hasRecord(string|array $recordAssertions, Level $level): bool { if (is_string($recordAssertions)) { $recordAssertions = ['message' => $recordAssertions]; } return $this->hasRecordThatPasses(function (LogRecord $rec) use ($recordAssertions) { if ($rec->message !== $recordAssertions['message']) { return false; } if (isset($recordAssertions['context']) && $rec->context !== $recordAssertions['context']) { return false; } return true; }, $level); } public function hasRecordThatContains(string $message, Level $level): bool { return $this->hasRecordThatPasses(fn (LogRecord $rec) => str_contains($rec->message, $message), $level); } public function hasRecordThatMatches(string $regex, Level $level): bool { return $this->hasRecordThatPasses(fn (LogRecord $rec) => preg_match($regex, $rec->message) > 0, $level); } /** * @phpstan-param callable(LogRecord, int): mixed $predicate */ public function hasRecordThatPasses(callable $predicate, Level $level): bool { $level = Logger::toMonologLevel($level); if (!isset($this->recordsByLevel[$level->value])) { return false; } foreach ($this->recordsByLevel[$level->value] as $i => $rec) { if ((bool) $predicate($rec, $i)) { return true; } } return false; } /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->recordsByLevel[$record->level->value][] = $record; $this->records[] = $record; } /** * @param mixed[] $args */ public function __call(string $method, array $args): bool { 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(Level::class.'::' . $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 . '()'); } } Handler/FormattableHandlerInterface.php 0000644 00000001365 15107322033 0014212 0 ustar 00 <?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. * * @return HandlerInterface self */ public function setFormatter(FormatterInterface $formatter): HandlerInterface; /** * Gets the formatter. */ public function getFormatter(): FormatterInterface; } Handler/FingersCrossedHandler.php 0000644 00000020011 15107322033 0013036 0 ustar 00 <?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 Closure; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Monolog\Level; use Monolog\Logger; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * 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> */ class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** * Handler or factory Closure($record, $this) * * @phpstan-var (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface */ protected Closure|HandlerInterface $handler; protected ActivationStrategyInterface $activationStrategy; protected bool $buffering = true; protected int $bufferSize; /** @var LogRecord[] */ protected array $buffer = []; protected bool $stopBuffering; protected Level|null $passthruLevel = null; protected bool $bubble; /** * @phpstan-param (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface $handler * * @param Closure|HandlerInterface $handler Handler or factory Closure($record|null, $fingersCrossedHandler). * @param int|string|Level|LogLevel::* $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|Level|LogLevel::*|null $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::*|ActivationStrategyInterface $activationStrategy * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $passthruLevel */ public function __construct(Closure|HandlerInterface $handler, int|string|Level|ActivationStrategyInterface $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, int|string|Level|null $passthruLevel = null) { if (null === $activationStrategy) { $activationStrategy = new ErrorLevelActivationStrategy(Level::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); } } /** * @inheritDoc */ public function isHandling(LogRecord $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(LogRecord $record): bool { if (\count($this->processors) > 0) { $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(): void { $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) { $passthruLevel = $this->passthruLevel; $this->buffer = array_filter($this->buffer, static function ($record) use ($passthruLevel) { return $passthruLevel->includes($record->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, this will trigger the handler's instantiation. */ public function getHandler(LogRecord $record = null): HandlerInterface { if (!$this->handler instanceof HandlerInterface) { $handler = ($this->handler)($record, $this); if (!$handler instanceof HandlerInterface) { throw new \RuntimeException("The factory Closure should return a HandlerInterface"); } $this->handler = $handler; } 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.'); } } Handler/RollbarHandler.php 0000644 00000007021 15107322034 0011522 0 ustar 00 <?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\Level; use Rollbar\RollbarLogger; use Throwable; use Monolog\LogRecord; /** * 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 { protected RollbarLogger $rollbarLogger; /** * Records whether any log records have been added since the last flush of the rollbar notifier */ private bool $hasRecords = false; protected bool $initialized = false; /** * @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token */ public function __construct(RollbarLogger $rollbarLogger, int|string|Level $level = Level::Error, bool $bubble = true) { $this->rollbarLogger = $rollbarLogger; parent::__construct($level, $bubble); } /** * Translates Monolog log levels to Rollbar levels. * * @return 'debug'|'info'|'warning'|'error'|'critical' */ protected function toRollbarLevel(Level $level): string { return match ($level) { Level::Debug => 'debug', Level::Info => 'info', Level::Notice => 'info', Level::Warning => 'warning', Level::Error => 'error', Level::Critical => 'critical', Level::Alert => 'critical', Level::Emergency => 'critical', }; } /** * @inheritDoc */ protected function write(LogRecord $record): void { if (!$this->initialized) { // __destructor() doesn't get called on Fatal errors register_shutdown_function([$this, 'close']); $this->initialized = true; } $context = $record->context; $context = array_merge($context, $record->extra, [ 'level' => $this->toRollbarLevel($record->level), 'monolog_level' => $record->level->getName(), '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(): void { $this->flush(); parent::reset(); } } Handler/SendGridHandler.php 0000644 00000005570 15107322034 0011633 0 ustar 00 <?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\Level; /** * 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 */ protected string $apiUser; /** * The SendGrid API Key */ protected string $apiKey; /** * The email addresses to which the message will be sent */ protected string $from; /** * The email addresses to which the message will be sent * @var string[] */ protected array $to; /** * The subject of the email */ protected string $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 * * @throws MissingExtensionException If the curl extension is missing */ public function __construct(string $apiUser, string $apiKey, string $from, string|array $to, string $subject, int|string|Level $level = Level::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); } } Handler/NullHandler.php 0000644 00000002463 15107322034 0011044 0 ustar 00 <?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\Level; use Psr\Log\LogLevel; use Monolog\Logger; use Monolog\LogRecord; /** * 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> */ class NullHandler extends Handler { private Level $level; /** * @param string|int|Level $level The minimum logging level at which this handler will be triggered * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function __construct(string|int|Level $level = Level::Debug) { $this->level = Logger::toMonologLevel($level); } /** * @inheritDoc */ public function isHandling(LogRecord $record): bool { return $record->level->value >= $this->level->value; } /** * @inheritDoc */ public function handle(LogRecord $record): bool { return $record->level->value >= $this->level->value; } } Handler/NewRelicHandler.php 0000644 00000013307 15107322034 0011641 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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 { /** * @inheritDoc */ public function __construct( int|string|Level $level = Level::Error, bool $bubble = true, /** * Name of the New Relic application that will receive logs from this handler. */ protected string|null $appName = null, /** * 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? */ protected bool $explodeArrays = false, /** * Name of the current transaction */ protected string|null $transactionName = null ) { parent::__construct($level, $bubble); } /** * @inheritDoc */ protected function write(LogRecord $record): void { if (!$this->isNewRelicEnabled()) { throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler'); } if (null !== ($appName = $this->getAppName($record->context))) { $this->setNewRelicAppName($appName); } if (null !== ($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. */ 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 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(); } } Handler/ZendMonitorHandler.php 0000644 00000005442 15107322035 0012403 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * Handler sending logs to Zend Monitor * * @author Christian Bergau <cbergau86@gmail.com> * @author Jason Davis <happydude@jasondavis.net> */ class ZendMonitorHandler extends AbstractProcessingHandler { /** * @throws MissingExtensionException */ public function __construct(int|string|Level $level = Level::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' ); } parent::__construct($level, $bubble); } /** * Translates Monolog log levels to ZendMonitor levels. */ protected function toZendMonitorLevel(Level $level): int { return match ($level) { Level::Debug => \ZEND_MONITOR_EVENT_SEVERITY_INFO, Level::Info => \ZEND_MONITOR_EVENT_SEVERITY_INFO, Level::Notice => \ZEND_MONITOR_EVENT_SEVERITY_INFO, Level::Warning => \ZEND_MONITOR_EVENT_SEVERITY_WARNING, Level::Error => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, Level::Critical => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, Level::Alert => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, Level::Emergency => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, }; } /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->writeZendMonitorCustomEvent( $record->level->getName(), $record->message, $record->formatted, $this->toZendMonitorLevel($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<mixed> $formatted Displayed in Custom Variables tab * @param int $severity Set the event severity level (-1,0,1) */ 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(); } } Handler/PushoverHandler.php 0000644 00000017677 15107322035 0011763 0 ustar 00 <?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\Level; use Monolog\Logger; use Monolog\Utils; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * Sends notifications through the pushover api to mobile phones * * @author Sebastian Göttschkes <sebastian.goettschkes@googlemail.com> * @see https://www.pushover.net/api */ class PushoverHandler extends SocketHandler { private string $token; /** @var array<int|string> */ private array $users; private string $title; private string|int|null $user = null; private int $retry; private int $expire; private Level $highPriorityLevel; private Level $emergencyLevel; private bool $useFormattedMessage = false; /** * All parameters that can be sent to Pushover * @see https://pushover.net/api * @var array<string, bool> */ private array $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 array $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 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). * * @param int|string|Level|LogLevel::* $highPriorityLevel The minimum logging level at which this handler will start * sending "high priority" requests to the Pushover API * @param int|string|Level|LogLevel::* $emergencyLevel The minimum logging level at which this handler will start * sending "emergency" requests to the Pushover API * * * @phpstan-param string|array<int|string> $users * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $highPriorityLevel * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $emergencyLevel */ public function __construct( string $token, $users, ?string $title = null, int|string|Level $level = Level::Critical, bool $bubble = true, bool $useSSL = true, int|string|Level $highPriorityLevel = Level::Critical, int|string|Level $emergencyLevel = Level::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(LogRecord $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } private function buildContent(LogRecord $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 ($record->level->value >= $this->emergencyLevel->value) { $dataArray['priority'] = 2; $dataArray['retry'] = $this->retry; $dataArray['expire'] = $this->expire; } elseif ($record->level->value >= $this->highPriorityLevel->value) { $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, true)) { 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(LogRecord $record): void { foreach ($this->users as $user) { $this->user = $user; parent::write($record); $this->closeSocket(); } $this->user = null; } /** * @param int|string|Level|LogLevel::* $level * @return $this * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function setHighPriorityLevel(int|string|Level $level): self { $this->highPriorityLevel = Logger::toMonologLevel($level); return $this; } /** * @param int|string|Level|LogLevel::* $level * @return $this * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function setEmergencyLevel(int|string|Level $level): self { $this->emergencyLevel = Logger::toMonologLevel($level); return $this; } /** * Use the formatted message? * * @return $this */ public function useFormattedMessage(bool $useFormattedMessage): self { $this->useFormattedMessage = $useFormattedMessage; return $this; } } Handler/ProcessableHandlerTrait.php 0000644 00000003122 15107322035 0013372 0 ustar 00 <?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; use Monolog\LogRecord; /** * Helper trait for implementing ProcessableInterface * * @author Jordi Boggiano <j.boggiano@seld.be> */ trait ProcessableHandlerTrait { /** * @var callable[] * @phpstan-var array<(callable(LogRecord): LogRecord)|ProcessorInterface> */ protected array $processors = []; /** * @inheritDoc */ public function pushProcessor(callable $callback): HandlerInterface { array_unshift($this->processors, $callback); return $this; } /** * @inheritDoc */ public function popProcessor(): callable { if (\count($this->processors) === 0) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } protected function processRecord(LogRecord $record): LogRecord { 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(); } } } } Handler/GroupHandler.php 0000644 00000006306 15107322035 0011227 0 ustar 00 <?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; use Monolog\LogRecord; /** * Forwards records to multiple handlers * * @author Lenar Lõhmus <lenar@city.ee> */ class GroupHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface { use ProcessableHandlerTrait; /** @var HandlerInterface[] */ protected array $handlers; protected bool $bubble; /** * @param HandlerInterface[] $handlers Array of Handlers. * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * * @throws \InvalidArgumentException if an unsupported handler is set */ 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(LogRecord $record): bool { foreach ($this->handlers as $handler) { if ($handler->isHandling($record)) { return true; } } return false; } /** * @inheritDoc */ public function handle(LogRecord $record): bool { if (\count($this->processors) > 0) { $record = $this->processRecord($record); } foreach ($this->handlers as $handler) { $handler->handle(clone $record); } return false === $this->bubble; } /** * @inheritDoc */ public function handleBatch(array $records): void { if (\count($this->processors) > 0) { $processed = []; foreach ($records as $record) { $processed[] = $this->processRecord($record); } $records = $processed; } foreach ($this->handlers as $handler) { $handler->handleBatch(array_map(fn ($record) => clone $record, $records)); } } public function reset(): void { $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; } } Handler/LogmaticHandler.php 0000644 00000005111 15107322036 0011664 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LogmaticFormatter; use Monolog\LogRecord; /** * @author Julien Breux <julien.breux@gmail.com> */ class LogmaticHandler extends SocketHandler { private string $logToken; private string $hostname; private string $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 = Level::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(LogRecord $record): string { return $this->logToken . ' ' . $record->formatted; } /** * @inheritDoc */ protected function getDefaultFormatter(): FormatterInterface { $formatter = new LogmaticFormatter(); if ($this->hostname !== '') { $formatter->setHostname($this->hostname); } if ($this->appName !== '') { $formatter->setAppName($this->appName); } return $formatter; } } Handler/CouchDBHandler.php 0000644 00000005062 15107322036 0011401 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * CouchDB handler * * @author Markus Bachmann <markus.bachmann@bachi.biz> * @phpstan-type Options array{ * host: string, * port: int, * dbname: string, * username: string|null, * password: string|null * } * @phpstan-type InputOptions array{ * host?: string, * port?: int, * dbname?: string, * username?: string|null, * password?: string|null * } */ class CouchDBHandler extends AbstractProcessingHandler { /** * @var mixed[] * @phpstan-var Options */ private array $options; /** * @param mixed[] $options * * @phpstan-param InputOptions $options */ public function __construct(array $options = [], int|string|Level $level = Level::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(LogRecord $record): void { $basicAuth = null; if (null !== $this->options['username'] && null !== $this->options['password']) { $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); } } Handler/RedisHandler.php 0000644 00000005265 15107322036 0011205 0 ustar 00 <?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\Level; use Monolog\LogRecord; use Predis\Client as Predis; use Redis; /** * 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> */ class RedisHandler extends AbstractProcessingHandler { /** @var Predis<Predis>|Redis */ private Predis|Redis $redisClient; private string $redisKey; protected int $capSize; /** * @param Predis<Predis>|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(Predis|Redis $redis, string $key, int|string|Level $level = Level::Debug, bool $bubble = true, int $capSize = 0) { $this->redisClient = $redis; $this->redisKey = $key; $this->capSize = $capSize; parent::__construct($level, $bubble); } /** * @inheritDoc */ protected function write(LogRecord $record): void { if ($this->capSize > 0) { $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 */ protected function writeCapped(LogRecord $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(); } } Handler/SyslogUdpHandler.php 0000644 00000011176 15107322036 0012066 0 ustar 00 <?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\Handler\SyslogUdp\UdpSocket; use Monolog\Level; use Monolog\LogRecord; 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 array $dateFormats = [ self::RFC3164 => 'M d H:i:s', self::RFC5424 => \DateTime::RFC3339, self::RFC5424e => \DateTime::RFC3339_EXTENDED, ]; protected UdpSocket $socket; protected string $ident; /** @var self::RFC* */ protected int $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 when there is no socket extension * * @phpstan-param self::RFC* $rfc */ public function __construct(string $host, int $port = 514, string|int $facility = LOG_USER, int|string|Level $level = Level::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(LogRecord $record): void { $lines = $this->splitMessageIntoLines($record->formatted); $header = $this->makeCommonSyslogHeader($this->toSyslogPriority($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; $pid = getmypid(); if (false === $pid) { $pid = '-'; } $hostname = gethostname(); if (false === $hostname) { $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 * * @return $this */ public function setSocket(UdpSocket $socket): self { $this->socket = $socket; return $this; } } Handler/StreamHandler.php 0000644 00000014760 15107322037 0011373 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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> */ class StreamHandler extends AbstractProcessingHandler { protected const MAX_CHUNK_SIZE = 2147483647; /** 10MB */ protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024; protected int $streamChunkSize; /** @var resource|null */ protected $stream; protected string|null $url = null; private string|null $errorMessage = null; protected int|null $filePermission; protected bool $useLocking; /** @var true|null */ private bool|null $dirCreated = null; /** * @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 * * @throws \InvalidArgumentException If stream is not a resource or string */ public function __construct($stream, int|string|Level $level = Level::Debug, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) { 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->filePermission = $filePermission; $this->useLocking = $useLocking; } /** * @inheritDoc */ public function close(): void { if (null !== $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 */ public function getUrl(): ?string { return $this->url; } public function getStreamChunkSize(): int { return $this->streamChunkSize; } /** * @inheritDoc */ protected function write(LogRecord $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([$this, 'customErrorHandler']); try { $stream = fopen($url, 'a'); 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 ($this->useLocking) { // ignoring errors here, there's not much we can do about them flock($stream, LOCK_EX); } $this->streamWrite($stream, $record); if ($this->useLocking) { flock($stream, LOCK_UN); } } /** * Write to stream * @param resource $stream */ protected function streamWrite($stream, LogRecord $record): void { fwrite($stream, (string) $record->formatted); } private function customErrorHandler(int $code, string $msg): bool { $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $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 (true === $this->dirCreated) { return; } $dir = $this->getDirFromStream($url); if (null !== $dir && !is_dir($dir)) { $this->errorMessage = null; set_error_handler([$this, 'customErrorHandler']); $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; } } Handler/BrowserConsoleHandler.php 0000644 00000022025 15107322037 0013077 0 ustar 00 <?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\LogRecord; use Monolog\Level; use function count; use function headers_list; use function stripos; /** * Handler sending logs to browser's javascript console with no browser extension required * * @author Olivier Poitrey <rs@dailymotion.com> */ class BrowserConsoleHandler extends AbstractProcessingHandler { protected static bool $initialized = false; /** @var LogRecord[] */ protected static array $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(LogRecord $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) > 0) { if ($format === self::FORMAT_HTML) { static::writeOutput('<script>' . self::generateScript() . '</script>'); } else { // js format static::writeOutput(self::generateScript()); } static::resetStatic(); } } public function close(): void { self::resetStatic(); } public function reset(): void { 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 = self::dump('Context', $record->context); $extra = self::dump('Extra', $record->extra); if (\count($context) === 0 && \count($extra) === 0) { $script[] = self::call_array(self::getConsoleMethodForLevel($record->level), self::handleStyles($record->formatted)); } else { $script = array_merge( $script, [self::call_array('groupCollapsed', self::handleStyles($record->formatted))], $context, $extra, [self::call('groupEnd')] ); } } return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);"; } private static function getConsoleMethodForLevel(Level $level): string { return match ($level) { Level::Debug => 'debug', Level::Info, Level::Notice => 'info', Level::Warning => 'warn', Level::Error, Level::Critical, Level::Alert, Level::Emergency => 'error', }; } /** * @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[] = self::quote(self::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[] = self::quote('font-weight: normal'); $args[] = self::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 (\count($dict) === 0) { return $script; } $script[] = self::call('log', self::quote('%c%s'), self::quote('font-weight: bold'), self::quote($title)); foreach ($dict as $key => $value) { $value = json_encode($value); if (false === $value) { $value = self::quote(''); } $script[] = self::call('log', self::quote('%s: %o'), self::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 self::call_array($method, $args); } /** * @param mixed[] $args */ private static function call_array(string $method, array $args): string { return 'c.' . $method . '(' . implode(', ', $args) . ');'; } } Handler/CubeHandler.php 0000644 00000012476 15107322037 0011020 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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 { private ?\Socket $udpConnection = null; private ?\CurlHandle $httpConnection = null; private string $scheme; private string $host; private int $port; /** @var string[] */ private array $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, int|string|Level $level = Level::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, true)) { throw new \UnexpectedValueException( 'Invalid protocol (' . $urlInfo['scheme'] . ').' . ' Valid options are ' . implode(', ', $this->acceptedSchemes) ); } $this->scheme = $urlInfo['scheme']; $this->host = $urlInfo['host']; $this->port = $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(LogRecord $record): void { $date = $record->datetime; $data = ['time' => $date->format('Y-m-d\TH:i:s.uO')]; $context = $record->context; if (isset($context['type'])) { $data['type'] = $context['type']; unset($context['type']); } else { $data['type'] = $record->channel; } $data['data'] = $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 (null === $this->udpConnection) { $this->connectUdp(); } if (null === $this->udpConnection) { throw new \LogicException('No UDP socket could be opened'); } socket_send($this->udpConnection, $data, strlen($data), 0); } private function writeHttp(string $data): void { if (null === $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); } } Handler/BufferHandler.php 0000644 00000010702 15107322040 0011333 0 ustar 00 <?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\Level; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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> */ class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; protected HandlerInterface $handler; protected int $bufferSize = 0; protected int $bufferLimit; protected bool $flushOnOverflow; /** @var LogRecord[] */ protected array $buffer = []; protected bool $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, int|string|Level $level = Level::Debug, bool $bubble = true, bool $flushOnOverflow = false) { parent::__construct($level, $bubble); $this->handler = $handler; $this->bufferLimit = $bufferLimit; $this->flushOnOverflow = $flushOnOverflow; } /** * @inheritDoc */ public function handle(LogRecord $record): bool { if ($record->level->isLowerThan($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 (\count($this->processors) > 0) { $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(): void { $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.'); } } Handler/TelegramBotHandler.php 0000644 00000022307 15107322040 0012333 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * Handler sends logs to Telegram using Telegram Bot API. * * How to use: * 1) Create a Telegram bot with https://telegram.me/BotFather; * 2) Create a Telegram channel or a group where logs will be recorded; * 3) Add the created bot from step 1 to the created channel/group from step 2. * * In order to create an instance of TelegramBotHandler use * 1. The Telegram bot API key from step 1 * 2. The channel name with the `@` prefix if you created a public channel (e.g. `@my_public_channel`), * or the channel ID with the `-100` prefix if you created a private channel (e.g. `-1001234567890`), * or the group ID from step 2 (e.g. `-1234567890`). * * @link https://core.telegram.org/bots/api * * @author Mazur Alexandr <alexandrmazur96@gmail.com> */ 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. */ private string $apiKey; /** * Telegram channel name. * Since to start with '@' symbol as prefix. */ private string $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 */ private string|null $parseMode; /** * Disables link previews for links in the message. */ private bool|null $disableWebPagePreview; /** * Sends the message silently. Users will receive a notification with no sound. */ private bool|null $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. */ private bool $splitLongMessages; /** * Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests). */ private bool $delayBetweenMessages; /** * Telegram message thread id, unique identifier for the target message thread (topic) of the forum; for forum supergroups only * See how to get the `message_thread_id` https://stackoverflow.com/a/75178418 */ private int|null $topic; /** * @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 * @param int $topic Telegram message thread id, unique identifier for the target message thread (topic) of the forum * @throws MissingExtensionException If the curl extension is missing */ public function __construct( string $apiKey, string $channel, $level = Level::Debug, bool $bubble = true, string $parseMode = null, bool $disableWebPagePreview = null, bool $disableNotification = null, bool $splitLongMessages = false, bool $delayBetweenMessages = false, int $topic = null ) { 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); $this->setTopic($topic); } /** * @return $this */ public function setParseMode(string $parseMode = null): self { if ($parseMode !== null && !in_array($parseMode, self::AVAILABLE_PARSE_MODES, true)) { throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.'); } $this->parseMode = $parseMode; return $this; } /** * @return $this */ public function disableWebPagePreview(bool $disableWebPagePreview = null): self { $this->disableWebPagePreview = $disableWebPagePreview; return $this; } /** * @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. * * @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). * * @return $this */ public function delayBetweenMessages(bool $delayBetweenMessages = false): self { $this->delayBetweenMessages = $delayBetweenMessages; return $this; } /** * @return $this */ public function setTopic(int $topic = null): self { $this->topic = $topic; return $this; } /** * @inheritDoc */ public function handleBatch(array $records): void { $messages = []; foreach ($records as $record) { if (!$this->isHandling($record)) { continue; } if (\count($this->processors) > 0) { $record = $this->processRecord($record); } $messages[] = $record; } if (\count($messages) > 0) { $this->send((string) $this->getFormatter()->formatBatch($messages)); } } /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->send($record->formatted); } /** * Send request to @link https://api.telegram.org/bot on SendMessage action. */ 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); $params = [ 'text' => $message, 'chat_id' => $this->channel, 'parse_mode' => $this->parseMode, 'disable_web_page_preview' => $this->disableWebPagePreview, 'disable_notification' => $this->disableNotification, ]; if ($this->topic !== null) { $params['message_thread_id'] = $this->topic; } curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); $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 * @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); } } Handler/FilterHandler.php 0000644 00000015743 15107322040 0011361 0 ustar 00 <?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 Closure; use Monolog\Level; use Monolog\Logger; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * 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> */ class FilterHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** * Handler or factory Closure($record, $this) * * @phpstan-var (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface */ protected Closure|HandlerInterface $handler; /** * Minimum level for logs that are passed to handler * * @var bool[] Map of Level value => true * @phpstan-var array<value-of<Level::VALUES>, true> */ protected array $acceptedLevels; /** * Whether the messages that are handled can bubble up the stack or not */ protected bool $bubble; /** * @phpstan-param (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface $handler * * @param Closure|HandlerInterface $handler Handler or factory Closure($record|null, $filterHandler). * @param int|string|Level|array<int|string|Level|LogLevel::*> $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided * @param int|string|Level|LogLevel::* $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 value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::*|array<value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::*> $minLevelOrList * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $maxLevel */ public function __construct(Closure|HandlerInterface $handler, int|string|Level|array $minLevelOrList = Level::Debug, int|string|Level $maxLevel = Level::Emergency, bool $bubble = true) { $this->handler = $handler; $this->bubble = $bubble; $this->setAcceptedLevels($minLevelOrList, $maxLevel); } /** * @phpstan-return list<Level> List of levels */ public function getAcceptedLevels(): array { return array_map(fn (int $level) => Level::from($level), array_keys($this->acceptedLevels)); } /** * @param int|string|Level|LogLevel::*|array<int|string|Level|LogLevel::*> $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided * @param int|string|Level|LogLevel::* $maxLevel Maximum level or level name to accept, only used if $minLevelOrList is not an array * @return $this * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::*|array<value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::*> $minLevelOrList * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $maxLevel */ public function setAcceptedLevels(int|string|Level|array $minLevelOrList = Level::Debug, int|string|Level $maxLevel = Level::Emergency): self { if (is_array($minLevelOrList)) { $acceptedLevels = array_map(Logger::toMonologLevel(...), $minLevelOrList); } else { $minLevelOrList = Logger::toMonologLevel($minLevelOrList); $maxLevel = Logger::toMonologLevel($maxLevel); $acceptedLevels = array_values(array_filter(Level::cases(), fn (Level $level) => $level->value >= $minLevelOrList->value && $level->value <= $maxLevel->value)); } $this->acceptedLevels = []; foreach ($acceptedLevels as $level) { $this->acceptedLevels[$level->value] = true; } return $this; } /** * @inheritDoc */ public function isHandling(LogRecord $record): bool { return isset($this->acceptedLevels[$record->level->value]); } /** * @inheritDoc */ public function handle(LogRecord $record): bool { if (!$this->isHandling($record)) { return false; } if (\count($this->processors) > 0) { $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, this will trigger the handler's instantiation. */ public function getHandler(LogRecord $record = null): HandlerInterface { if (!$this->handler instanceof HandlerInterface) { $handler = ($this->handler)($record, $this); if (!$handler instanceof HandlerInterface) { throw new \RuntimeException("The factory Closure should return a HandlerInterface"); } $this->handler = $handler; } 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(): void { $this->resetProcessors(); if ($this->getHandler() instanceof ResettableInterface) { $this->getHandler()->reset(); } } } Handler/FirePHPHandler.php 0000644 00000012041 15107322041 0011356 0 ustar 00 <?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; use Monolog\LogRecord; /** * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol. * * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com> */ 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 */ protected static bool $initialized = false; /** * Shared static message index between potentially multiple handlers */ protected static int $messageIndex = 1; protected static bool $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() */ protected function createRecordHeader(LogRecord $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(LogRecord $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 (isset($_SERVER['HTTP_USER_AGENT']) && 1 === preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) { return true; } return isset($_SERVER['HTTP_X_FIREPHP_VERSION']); } } Handler/WebRequestRecognizerTrait.php 0000644 00000000770 15107322041 0013753 0 ustar 00 <?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 */ protected function isWebRequest(): bool { return 'cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI; } } Handler/AmqpHandler.php 0000644 00000011315 15107322041 0011022 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\JsonFormatter; use PhpAmqpLib\Message\AMQPMessage; use PhpAmqpLib\Channel\AMQPChannel; use AMQPExchange; use Monolog\LogRecord; class AmqpHandler extends AbstractProcessingHandler { protected AMQPExchange|AMQPChannel $exchange; /** @var array<string, mixed> */ private array $extraAttributes = []; protected string $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(AMQPExchange|AMQPChannel $exchange, ?string $exchangeName = null, int|string|Level $level = Level::Debug, bool $bubble = true) { if ($exchange instanceof AMQPChannel) { $this->exchangeName = (string) $exchangeName; } elseif ($exchangeName !== null) { @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); } /** * @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 $this */ public function setExtraAttributes(array $extraAttributes): self { $this->extraAttributes = $extraAttributes; return $this; } /** * @inheritDoc */ protected function write(LogRecord $record): void { $data = $record->formatted; $routingKey = $this->getRoutingKey($record); if ($this->exchange instanceof AMQPExchange) { $attributes = [ 'delivery_mode' => 2, 'content_type' => 'application/json', ]; if (\count($this->extraAttributes) > 0) { $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; } $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 */ protected function getRoutingKey(LogRecord $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 (\count($this->extraAttributes) > 0) { $attributes = array_merge($attributes, $this->extraAttributes); } return new AMQPMessage($data, $attributes); } /** * @inheritDoc */ protected function getDefaultFormatter(): FormatterInterface { return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); } } Handler/LogglyHandler.php 0000644 00000010057 15107322042 0011364 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LogglyFormatter; use function array_key_exists; use CurlHandle; use Monolog\LogRecord; /** * 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 CurlHandle[] */ protected array $curlHandlers = []; protected string $token; /** @var string[] */ protected array $tag = []; /** * @param string $token API token supplied by Loggly * * @throws MissingExtensionException If the curl extension is missing */ public function __construct(string $token, int|string|Level $level = Level::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. */ protected function getCurlHandler(string $endpoint): CurlHandle { 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. */ private function loadCurlHandle(string $endpoint): CurlHandle { $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 * @return $this */ public function setTag(string|array $tag): self { if ('' === $tag || [] === $tag) { $this->tag = []; } else { $this->tag = is_array($tag) ? $tag : [$tag]; } return $this; } /** * @param string[]|string $tag * @return $this */ public function addTag(string|array $tag): self { if ('' !== $tag) { $tag = is_array($tag) ? $tag : [$tag]; $this->tag = array_unique(array_merge($this->tag, $tag)); } return $this; } protected function write(LogRecord $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->value >= $level->value); }); if (\count($records) > 0) { $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 (\count($this->tag) > 0) { $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(); } } Handler/SyslogHandler.php 0000644 00000003252 15107322042 0011406 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\LogRecord; /** * 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 { protected string $ident; protected int $logopts; /** * @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, string|int $facility = LOG_USER, int|string|Level $level = Level::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(LogRecord $record): void { openlog($this->ident, $this->logopts, $this->facility); syslog($this->toSyslogPriority($record->level), (string) $record->formatted); } } Handler/SlackWebhookHandler.php 0000644 00000007624 15107322042 0012511 0 ustar 00 <?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\Level; use Monolog\Utils; use Monolog\Handler\Slack\SlackRecord; use Monolog\LogRecord; /** * 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 */ private string $webhookUrl; /** * Instance of the SlackRecord util class preparing data for Slack API. */ private SlackRecord $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'] * * @throws MissingExtensionException If the curl extension is missing */ public function __construct( string $webhookUrl, ?string $channel = null, ?string $username = null, bool $useAttachment = true, ?string $iconEmoji = null, bool $useShortAttachment = false, bool $includeContextAndExtra = false, $level = Level::Critical, bool $bubble = true, array $excludeFields = [] ) { 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(LogRecord $record): void { $postData = $this->slackRecord->getSlackData($record); $postString = Utils::jsonEncode($postData); $ch = curl_init(); $options = [ CURLOPT_URL => $this->webhookUrl, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['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; } } Handler/GelfHandler.php 0000644 00000002656 15107322042 0011012 0 ustar 00 <?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\Level; use Monolog\Formatter\GelfMessageFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\LogRecord; /** * 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 PublisherInterface $publisher; /** * @param PublisherInterface $publisher a gelf publisher object */ public function __construct(PublisherInterface $publisher, int|string|Level $level = Level::Debug, bool $bubble = true) { parent::__construct($level, $bubble); $this->publisher = $publisher; } /** * @inheritDoc */ protected function write(LogRecord $record): void { $this->publisher->publish($record->formatted); } /** * @inheritDoc */ protected function getDefaultFormatter(): FormatterInterface { return new GelfMessageFormatter(); } } Handler/MissingExtensionException.php 0000644 00000000731 15107322043 0014015 0 ustar 00 <?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 { } Handler/SocketHandler.php 0000644 00000027522 15107322043 0011365 0 ustar 00 <?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\Level; use Monolog\LogRecord; /** * 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 */ class SocketHandler extends AbstractProcessingHandler { private string $connectionString; private float $connectionTimeout; /** @var resource|null */ private $resource; private float $timeout; private float $writingTimeout; private int|null $lastSentBytes = null; private int|null $chunkSize; private bool $persistent; private int|null $errno = null; private string|null $errstr = null; private float|null $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 = Level::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(LogRecord $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. * * @return $this */ 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 * @return $this */ 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 * @return $this */ 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 * @return $this */ 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. * * @return $this */ 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 */ 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 */ protected function streamSetTimeout(): bool { $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|false */ protected function streamSetChunkSize(): int|bool { 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|false */ protected function fwrite(string $data): int|bool { 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(): array|bool { 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(); } protected function generateDataStream(LogRecord $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 (null !== $this->chunkSize && false === $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) && (bool) $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) - (float) $this->lastWritingAt) >= $this->writingTimeout) { $this->closeSocket(); return true; } return false; } } Handler/ElasticaHandler.php 0000644 00000007126 15107322043 0011660 0 ustar 00 <?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\Level; use Elastica\Client; use Elastica\Exception\ExceptionInterface; use Monolog\LogRecord; /** * 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> * @phpstan-type Options array{ * index: string, * type: string, * ignore_error: bool * } * @phpstan-type InputOptions array{ * index?: string, * type?: string, * ignore_error?: bool * } */ class ElasticaHandler extends AbstractProcessingHandler { protected Client $client; /** * @var mixed[] Handler config options * @phpstan-var Options */ protected array $options; /** * @param Client $client Elastica Client object * @param mixed[] $options Handler configuration * * @phpstan-param InputOptions $options */ public function __construct(Client $client, array $options = [], int|string|Level $level = Level::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(LogRecord $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[] * * @phpstan-return Options */ 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); } } } } Handler/AbstractSyslogHandler.php 0000644 00000006225 15107322044 0013077 0 ustar 00 <?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\Level; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; /** * Common syslog functionality */ abstract class AbstractSyslogHandler extends AbstractProcessingHandler { protected int $facility; /** * List of valid log facility names. * @var array<string, int> */ protected array $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, ]; /** * Translates Monolog log levels to syslog log priorities. */ protected function toSyslogPriority(Level $level): int { return $level->toRFC5424Level(); } /** * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant */ public function __construct(string|int $facility = \LOG_USER, int|string|Level $level = Level::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%'); } } Processor/WebProcessor.php 0000644 00000006616 15107322044 0011660 0 ustar 00 <?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\Processor; use ArrayAccess; use Monolog\LogRecord; /** * Injects url/method and remote IP of the current web request in all records * * @author Jordi Boggiano <j.boggiano@seld.be> */ class WebProcessor implements ProcessorInterface { /** * @var array<string, mixed>|ArrayAccess<string, mixed> */ protected array|ArrayAccess $serverData; /** * Default fields * * Array is structured as [key in record.extra => key in $serverData] * * @var array<string, string> */ protected array $extraFields = [ 'url' => 'REQUEST_URI', 'ip' => 'REMOTE_ADDR', 'http_method' => 'REQUEST_METHOD', 'server' => 'SERVER_NAME', 'referrer' => 'HTTP_REFERER', 'user_agent' => 'HTTP_USER_AGENT', ]; /** * @param array<string, mixed>|ArrayAccess<string, mixed>|null $serverData Array or object w/ ArrayAccess that provides access to the $_SERVER data * @param array<string, string>|array<string>|null $extraFields Field names and the related key inside $serverData to be added (or just a list of field names to use the default configured $serverData mapping). If not provided it defaults to: [url, ip, http_method, server, referrer] + unique_id if present in server data */ public function __construct(array|ArrayAccess|null $serverData = null, array|null $extraFields = null) { if (null === $serverData) { $this->serverData = &$_SERVER; } else { $this->serverData = $serverData; } $defaultEnabled = ['url', 'ip', 'http_method', 'server', 'referrer']; if (isset($this->serverData['UNIQUE_ID'])) { $this->extraFields['unique_id'] = 'UNIQUE_ID'; $defaultEnabled[] = 'unique_id'; } if (null === $extraFields) { $extraFields = $defaultEnabled; } if (isset($extraFields[0])) { foreach (array_keys($this->extraFields) as $fieldName) { if (!in_array($fieldName, $extraFields, true)) { unset($this->extraFields[$fieldName]); } } } else { $this->extraFields = $extraFields; } } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { // skip processing if for some reason request data // is not present (CLI or wonky SAPIs) if (!isset($this->serverData['REQUEST_URI'])) { return $record; } $record->extra = $this->appendExtraFields($record->extra); return $record; } /** * @return $this */ public function addExtraField(string $extraName, string $serverName): self { $this->extraFields[$extraName] = $serverName; return $this; } /** * @param mixed[] $extra * @return mixed[] */ private function appendExtraFields(array $extra): array { foreach ($this->extraFields as $extraName => $serverName) { $extra[$extraName] = $this->serverData[$serverName] ?? null; } return $extra; } } Processor/MemoryProcessor.php 0000644 00000003414 15107322044 0012404 0 ustar 00 <?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\Processor; /** * Some methods that are common for all memory processors * * @author Rob Jensen */ abstract class MemoryProcessor implements ProcessorInterface { /** * @var bool If true, get the real size of memory allocated from system. Else, only the memory used by emalloc() is reported. */ protected bool $realUsage; /** * @var bool If true, then format memory size to human readable string (MB, KB, B depending on size) */ protected bool $useFormatting; /** * @param bool $realUsage Set this to true to get the real size of memory allocated from system. * @param bool $useFormatting If true, then format memory size to human readable string (MB, KB, B depending on size) */ public function __construct(bool $realUsage = true, bool $useFormatting = true) { $this->realUsage = $realUsage; $this->useFormatting = $useFormatting; } /** * Formats bytes into a human readable string if $this->useFormatting is true, otherwise return $bytes as is * * @return string|int Formatted string if $this->useFormatting is true, otherwise return $bytes as int */ protected function formatBytes(int $bytes) { if (!$this->useFormatting) { return $bytes; } if ($bytes > 1024 * 1024) { return round($bytes / 1024 / 1024, 2).' MB'; } elseif ($bytes > 1024) { return round($bytes / 1024, 2).' KB'; } return $bytes . ' B'; } } Processor/TagProcessor.php 0000644 00000002256 15107322045 0011653 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Adds a tags array into record * * @author Martijn Riemers */ class TagProcessor implements ProcessorInterface { /** @var string[] */ private array $tags; /** * @param string[] $tags */ public function __construct(array $tags = []) { $this->setTags($tags); } /** * @param string[] $tags * @return $this */ public function addTags(array $tags = []): self { $this->tags = array_merge($this->tags, $tags); return $this; } /** * @param string[] $tags * @return $this */ public function setTags(array $tags = []): self { $this->tags = $tags; return $this; } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { $record->extra['tags'] = $this->tags; return $record; } } Processor/MercurialProcessor.php 0000644 00000003574 15107322045 0013067 0 ustar 00 <?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\Processor; use Monolog\Level; use Monolog\Logger; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * Injects Hg branch and Hg revision number in all records * * @author Jonathan A. Schweder <jonathanschweder@gmail.com> */ class MercurialProcessor implements ProcessorInterface { private Level $level; /** @var array{branch: string, revision: string}|array<never>|null */ private static $cache = null; /** * @param int|string|Level $level The minimum logging level at which this Processor will be triggered * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function __construct(int|string|Level $level = Level::Debug) { $this->level = Logger::toMonologLevel($level); } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { // return if the level is not high enough if ($record->level->isLowerThan($this->level)) { return $record; } $record->extra['hg'] = self::getMercurialInfo(); return $record; } /** * @return array{branch: string, revision: string}|array<never> */ private static function getMercurialInfo(): array { if (self::$cache !== null) { return self::$cache; } $result = explode(' ', trim((string) shell_exec('hg id -nb'))); if (count($result) >= 3) { return self::$cache = [ 'branch' => $result[1], 'revision' => $result[2], ]; } return self::$cache = []; } } Processor/MemoryPeakUsageProcessor.php 0000644 00000001540 15107322045 0014171 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Injects memory_get_peak_usage in all records * * @see Monolog\Processor\MemoryProcessor::__construct() for options * @author Rob Jensen */ class MemoryPeakUsageProcessor extends MemoryProcessor { /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { $usage = memory_get_peak_usage($this->realUsage); if ($this->useFormatting) { $usage = $this->formatBytes($usage); } $record->extra['memory_peak_usage'] = $usage; return $record; } } Processor/ProcessIdProcessor.php 0000644 00000001173 15107322046 0013031 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Adds value of getmypid into records * * @author Andreas Hörnicke */ class ProcessIdProcessor implements ProcessorInterface { /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { $record->extra['process_id'] = getmypid(); return $record; } } Processor/ClosureContextProcessor.php 0000644 00000002750 15107322046 0014121 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Generates a context from a Closure if the Closure is the only value * in the context * * It helps reduce the performance impact of debug logs if they do * need to create lots of context information. If this processor is added * on the correct handler the context data will only be generated * when the logs are actually logged to that handler, which is useful when * using FingersCrossedHandler or other filtering handlers to conditionally * log records. */ class ClosureContextProcessor implements ProcessorInterface { public function __invoke(LogRecord $record): LogRecord { $context = $record->context; if (isset($context[0]) && 1 === \count($context) && $context[0] instanceof \Closure) { try { $context = $context[0](); } catch (\Throwable $e) { $context = [ 'error_on_context_generation' => $e->getMessage(), 'exception' => $e, ]; } if (!\is_array($context)) { $context = [$context]; } $record = $record->with(context: $context); } return $record; } } Processor/ProcessorInterface.php 0000644 00000001100 15107322046 0013024 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * An optional interface to allow labelling Monolog processors. * * @author Nicolas Grekas <p@tchwork.com> */ interface ProcessorInterface { /** * @return LogRecord The processed record */ public function __invoke(LogRecord $record); } Processor/HostnameProcessor.php 0000644 00000001340 15107322046 0012710 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Injects value of gethostname in all records */ class HostnameProcessor implements ProcessorInterface { private static string $host; public function __construct() { self::$host = (string) gethostname(); } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { $record->extra['hostname'] = self::$host; return $record; } } Processor/GitProcessor.php 0000644 00000003677 15107322047 0011675 0 ustar 00 <?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\Processor; use Monolog\Level; use Monolog\Logger; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * Injects Git branch and Git commit SHA in all records * * @author Nick Otter * @author Jordi Boggiano <j.boggiano@seld.be> */ class GitProcessor implements ProcessorInterface { private Level $level; /** @var array{branch: string, commit: string}|array<never>|null */ private static $cache = null; /** * @param int|string|Level|LogLevel::* $level The minimum logging level at which this Processor will be triggered * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function __construct(int|string|Level $level = Level::Debug) { $this->level = Logger::toMonologLevel($level); } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { // return if the level is not high enough if ($record->level->isLowerThan($this->level)) { return $record; } $record->extra['git'] = self::getGitInfo(); return $record; } /** * @return array{branch: string, commit: string}|array<never> */ private static function getGitInfo(): array { if (self::$cache !== null) { return self::$cache; } $branches = shell_exec('git branch -v --no-abbrev'); if (is_string($branches) && 1 === preg_match('{^\* (.+?)\s+([a-f0-9]{40})(?:\s|$)}m', $branches, $matches)) { return self::$cache = [ 'branch' => $matches[1], 'commit' => $matches[2], ]; } return self::$cache = []; } } Processor/MemoryUsageProcessor.php 0000644 00000001515 15107322047 0013374 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Injects memory_get_usage in all records * * @see Monolog\Processor\MemoryProcessor::__construct() for options * @author Rob Jensen */ class MemoryUsageProcessor extends MemoryProcessor { /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { $usage = memory_get_usage($this->realUsage); if ($this->useFormatting) { $usage = $this->formatBytes($usage); } $record->extra['memory_usage'] = $usage; return $record; } } Processor/PsrLogMessageProcessor.php 0000644 00000005760 15107322047 0013660 0 ustar 00 <?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\Processor; use Monolog\Utils; use Monolog\LogRecord; /** * Processes a record's message according to PSR-3 rules * * It replaces {foo} with the value from $context['foo'] * * @author Jordi Boggiano <j.boggiano@seld.be> */ class PsrLogMessageProcessor implements ProcessorInterface { public const SIMPLE_DATE = "Y-m-d\TH:i:s.uP"; private ?string $dateFormat; private bool $removeUsedContextFields; /** * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format * @param bool $removeUsedContextFields If set to true the fields interpolated into message gets unset */ public function __construct(?string $dateFormat = null, bool $removeUsedContextFields = false) { $this->dateFormat = $dateFormat; $this->removeUsedContextFields = $removeUsedContextFields; } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { if (false === strpos($record->message, '{')) { return $record; } $replacements = []; $context = $record->context; foreach ($context as $key => $val) { $placeholder = '{' . $key . '}'; if (strpos($record->message, $placeholder) === false) { continue; } if (null === $val || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { $replacements[$placeholder] = $val; } elseif ($val instanceof \DateTimeInterface) { if (null === $this->dateFormat && $val instanceof \Monolog\DateTimeImmutable) { // handle monolog dates using __toString if no specific dateFormat was asked for // so that it follows the useMicroseconds flag $replacements[$placeholder] = (string) $val; } else { $replacements[$placeholder] = $val->format($this->dateFormat ?? static::SIMPLE_DATE); } } elseif ($val instanceof \UnitEnum) { $replacements[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name; } elseif (is_object($val)) { $replacements[$placeholder] = '[object '.Utils::getClass($val).']'; } elseif (is_array($val)) { $replacements[$placeholder] = 'array'.Utils::jsonEncode($val, null, true); } else { $replacements[$placeholder] = '['.gettype($val).']'; } if ($this->removeUsedContextFields) { unset($context[$key]); } } return $record->with(message: strtr($record->message, $replacements), context: $context); } } Processor/UidProcessor.php 0000644 00000002724 15107322047 0011663 0 ustar 00 <?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\Processor; use Monolog\ResettableInterface; use Monolog\LogRecord; /** * Adds a unique identifier into records * * @author Simon Mönch <sm@webfactory.de> */ class UidProcessor implements ProcessorInterface, ResettableInterface { /** @var non-empty-string */ private string $uid; /** * @param int<1, 32> $length */ public function __construct(int $length = 7) { if ($length > 32 || $length < 1) { throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); } $this->uid = $this->generateUid($length); } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { $record->extra['uid'] = $this->uid; return $record; } public function getUid(): string { return $this->uid; } public function reset(): void { $this->uid = $this->generateUid(strlen($this->uid)); } /** * @param positive-int $length * @return non-empty-string */ private function generateUid(int $length): string { return substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length); } } Processor/LoadAverageProcessor.php 0000644 00000003135 15107322050 0013303 0 ustar 00 <?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\Processor; use Monolog\LogRecord; /** * Injects sys_getloadavg in all records @see https://www.php.net/manual/en/function.sys-getloadavg.php * * @author Johan Vlaar <johan.vlaar.1994@gmail.com> */ class LoadAverageProcessor implements ProcessorInterface { public const LOAD_1_MINUTE = 0; public const LOAD_5_MINUTE = 1; public const LOAD_15_MINUTE = 2; private const AVAILABLE_LOAD = [ self::LOAD_1_MINUTE, self::LOAD_5_MINUTE, self::LOAD_15_MINUTE, ]; /** * @var int */ protected $avgSystemLoad; /** * @param self::LOAD_* $avgSystemLoad */ public function __construct(int $avgSystemLoad = self::LOAD_1_MINUTE) { if (!in_array($avgSystemLoad, self::AVAILABLE_LOAD, true)) { throw new \InvalidArgumentException(sprintf('Invalid average system load: `%s`', $avgSystemLoad)); } $this->avgSystemLoad = $avgSystemLoad; } /** * {@inheritDoc} */ public function __invoke(LogRecord $record): LogRecord { if (!function_exists('sys_getloadavg')) { return $record; } $usage = sys_getloadavg(); if (false === $usage) { return $record; } $record->extra['load_average'] = $usage[$this->avgSystemLoad]; return $record; } } Processor/IntrospectionProcessor.php 0000644 00000007205 15107322050 0013773 0 ustar 00 <?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\Processor; use Monolog\Level; use Monolog\Logger; use Psr\Log\LogLevel; use Monolog\LogRecord; /** * Injects line/file:class/function where the log message came from * * Warning: This only works if the handler processes the logs directly. * If you put the processor on a handler that is behind a FingersCrossedHandler * for example, the processor will only be called once the trigger level is reached, * and all the log records will have the same file/line/.. data from the call that * triggered the FingersCrossedHandler. * * @author Jordi Boggiano <j.boggiano@seld.be> */ class IntrospectionProcessor implements ProcessorInterface { private Level $level; /** @var string[] */ private array $skipClassesPartials; private int $skipStackFramesCount; private const SKIP_FUNCTIONS = [ 'call_user_func', 'call_user_func_array', ]; /** * @param string|int|Level $level The minimum logging level at which this Processor will be triggered * @param string[] $skipClassesPartials * * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level */ public function __construct(int|string|Level $level = Level::Debug, array $skipClassesPartials = [], int $skipStackFramesCount = 0) { $this->level = Logger::toMonologLevel($level); $this->skipClassesPartials = array_merge(['Monolog\\'], $skipClassesPartials); $this->skipStackFramesCount = $skipStackFramesCount; } /** * @inheritDoc */ public function __invoke(LogRecord $record): LogRecord { // return if the level is not high enough if ($record->level->isLowerThan($this->level)) { return $record; } $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // skip first since it's always the current method array_shift($trace); // the call_user_func call is also skipped array_shift($trace); $i = 0; while ($this->isTraceClassOrSkippedFunction($trace, $i)) { if (isset($trace[$i]['class'])) { foreach ($this->skipClassesPartials as $part) { if (strpos($trace[$i]['class'], $part) !== false) { $i++; continue 2; } } } elseif (in_array($trace[$i]['function'], self::SKIP_FUNCTIONS, true)) { $i++; continue; } break; } $i += $this->skipStackFramesCount; // we should have the call source now $record->extra = array_merge( $record->extra, [ 'file' => $trace[$i - 1]['file'] ?? null, 'line' => $trace[$i - 1]['line'] ?? null, 'class' => $trace[$i]['class'] ?? null, 'callType' => $trace[$i]['type'] ?? null, 'function' => $trace[$i]['function'] ?? null, ] ); return $record; } /** * @param array<mixed> $trace */ private function isTraceClassOrSkippedFunction(array $trace, int $index): bool { if (!isset($trace[$index])) { return false; } return isset($trace[$index]['class']) || in_array($trace[$index]['function'], self::SKIP_FUNCTIONS, true); } } DateTimeImmutable.php 0000644 00000002041 15107322050 0010601 0 ustar 00 <?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; use DateTimeZone; /** * Overrides default json encoding of date time objects * * @author Menno Holtkamp * @author Jordi Boggiano <j.boggiano@seld.be> */ class DateTimeImmutable extends \DateTimeImmutable implements \JsonSerializable { private bool $useMicroseconds; public function __construct(bool $useMicroseconds, ?DateTimeZone $timezone = null) { $this->useMicroseconds = $useMicroseconds; parent::__construct('now', $timezone); } public function jsonSerialize(): string { if ($this->useMicroseconds) { return $this->format('Y-m-d\TH:i:s.uP'); } return $this->format('Y-m-d\TH:i:sP'); } public function __toString(): string { return $this->jsonSerialize(); } }