diff --git a/.php_cs b/.php_cs index 34eb329f..6fc3ca2e 100644 --- a/.php_cs +++ b/.php_cs @@ -24,14 +24,13 @@ return PhpCsFixer\Config::create() '@PSR2' => true, // some rules disabled as long as 1.x branch is maintained 'binary_operator_spaces' => array( - 'align_double_arrow' => null, - 'align_equals' => null, + 'default' => null, ), - 'blank_line_before_return' => true, - 'cast_spaces' => true, - 'header_comment' => array('header' => $header), + 'blank_line_before_statement' => ['statements' => ['continue', 'declare', 'return', 'throw', 'try']], + 'cast_spaces' => ['space' => 'single'], + 'header_comment' => ['header' => $header], 'include' => true, - 'method_separation' => true, + 'class_attributes_separation' => ['elements' => ['method']], 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_empty_statement' => true, diff --git a/.travis.yml b/.travis.yml index 6f621367..04516150 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ sudo: false dist: trusty php: - - 7.0 - 7.1 + - 7.2 - nightly cache: @@ -14,7 +14,7 @@ cache: matrix: include: - - php: 7.0 + - php: 7.1 env: deps=low fast_finish: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b1cd4585..8c033f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +### 1.24.0 (2018-06-xx) + + * Added ability to customize error handling at the Logger level using Logger::setExceptionHandler + * Added InsightOpsHandler to migrate users of the LogEntriesHandler + * Added protection to NormalizerHandler against circular and very deep structures, it now stops normalizing at a depth of 9 + * Added capture of stack traces to ErrorHandler when logging PHP errors + * Added forwarding of context info to FluentdFormatter + * Added SocketHandler::setChunkSize to override the default chunk size in case you must send large log lines to rsyslog for example + * Added ability to extend/override BrowserConsoleHandler + * Added SlackWebhookHandler::getWebhookUrl and SlackHandler::getToken to enable class extensibility + * Added SwiftMailerHandler::getSubjectFormatter to enable class extensibility + * Dropped official support for HHVM in test builds + * Fixed normalization of exception traces when call_user_func is used to avoid serializing objects and the data they contain + * Fixed naming of fields in Slack handler, all field names are now capitalized in all cases + * Fixed HipChatHandler bug where slack dropped messages randomly + * Fixed normalization of objects in Slack handlers + * Fixed support for PHP7's Throwable in NewRelicHandler + * Fixed race bug when StreamHandler sometimes incorrectly reported it failed to create a directory + * Fixed table row styling issues in HtmlFormatter + * Fixed RavenHandler dropping the message when logging exception + * Fixed WhatFailureGroupHandler skipping processors when using handleBatch + ### 1.23.0 (2017-06-19) * Improved SyslogUdpHandler's support for RFC5424 and added optional `$ident` argument diff --git a/LICENSE b/LICENSE index b97667f8..713dc045 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2017 Jordi Boggiano +Copyright (c) 2011-2018 Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ea18c304..82bd4e7e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Total Downloads](https://img.shields.io/packagist/dt/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog) [![Latest Stable Version](https://img.shields.io/packagist/v/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog) -[![Reference Status](https://www.versioneye.com/php/monolog:monolog/reference_badge.svg)](https://www.versioneye.com/php/monolog:monolog/references) Monolog sends your logs to files, sockets, inboxes, databases and various @@ -59,7 +58,7 @@ can also add your own there if you publish one. ### Requirements -- Monolog works with PHP 7.0 or above, use Monolog `^1.0` for PHP 5.3+ support. +- Monolog 2.x works with PHP 7.1 or above, use Monolog `^1.0` for PHP 5.3+ support. ### Submitting bugs and feature requests @@ -82,7 +81,7 @@ Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/mono - [Proton Micro Framework](https://github.com/alexbilbie/Proton) comes out of the box with Monolog. - [FuelPHP](http://fuelphp.com/) comes out of the box with Monolog. - [Equip Framework](https://github.com/equip/framework) comes out of the box with Monolog. -- [Yii 2](http://www.yiiframework.com/) is usable with Monolog via the [yii2-monolog](https://github.com/merorafael/yii2-monolog) plugin. +- [Yii 2](http://www.yiiframework.com/) is usable with Monolog via the [yii2-monolog](https://github.com/merorafael/yii2-monolog) or [yii2-psr-log-target](https://github.com/samdark/yii2-psr-log-target) plugins. - [Hawkbit Micro Framework](https://github.com/HawkBitPhp/hawkbit) comes out of the box with Monolog. ### Author diff --git a/composer.json b/composer.json index 7cfc82c0..ec6750a8 100644 --- a/composer.json +++ b/composer.json @@ -13,11 +13,11 @@ } ], "require": { - "php": "^7.0", + "php": "^7.1", "psr/log": "^1.0.1" }, "require-dev": { - "phpunit/phpunit": "^5.7", + "phpunit/phpunit": "^6.5", "graylog2/gelf-php": "^1.4.2", "sentry/sentry": "^0.13", "ruflin/elastica": ">=0.90 <3.0", @@ -30,6 +30,8 @@ "predis/predis": "^1.1", "phpspec/prophecy": "^1.6.1", "elasticsearch/elasticsearch": "^6.0" + "phpspec/prophecy": "^1.6.1", + "rollbar/rollbar": "^1.3" }, "suggest": { "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", @@ -56,7 +58,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.x-dev" } }, "scripts": { diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index 9987908c..fb7add9d 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -45,9 +45,11 @@ - [_SocketHandler_](../src/Monolog/Handler/SocketHandler.php): Logs records to [sockets](http://php.net/fsockopen), use this for UNIX and TCP sockets. See an [example](sockets.md). -- [_AmqpHandler_](../src/Monolog/Handler/AmqpHandler.php): Logs records to an [amqp](http://www.amqp.org/) compatible - server. Requires the [php-amqp](http://pecl.php.net/package/amqp) extension (1.0+). +- [_AmqpHandler_](../src/Monolog/Handler/AmqpHandler.php): Logs records to an [AMQP](http://www.amqp.org/) compatible + server. Requires the [php-amqp](http://pecl.php.net/package/amqp) extension (1.0+) or + [php-amqplib](https://github.com/php-amqplib/php-amqplib) library. - [_GelfHandler_](../src/Monolog/Handler/GelfHandler.php): Logs records to a [Graylog2](http://www.graylog2.org) server. + Requires package [graylog2/gelf-php](https://github.com/bzikarsky/gelf-php). - [_CubeHandler_](../src/Monolog/Handler/CubeHandler.php): Logs records to a [Cube](http://square.github.com/cube/) server. - [_RavenHandler_](../src/Monolog/Handler/RavenHandler.php): Logs records to a [Sentry](http://getsentry.com/) server using [raven](https://packagist.org/packages/raven/raven). @@ -57,6 +59,7 @@ - [_RollbarHandler_](../src/Monolog/Handler/RollbarHandler.php): Logs records to a [Rollbar](https://rollbar.com/) account. - [_SyslogUdpHandler_](../src/Monolog/Handler/SyslogUdpHandler.php): Logs records to a remote [Syslogd](http://www.rsyslog.com/) server. - [_LogEntriesHandler_](../src/Monolog/Handler/LogEntriesHandler.php): Logs records to a [LogEntries](http://logentries.com/) account. +- [_InsightOpsHandler_](../src/Monolog/Handler/InsightOpsHandler.php): Logs records to a [InsightOps](https://www.rapid7.com/products/insightops/) account. - [_LogmaticHandler_](../src/Monolog/Handler/LogmaticHandler.php): Logs records to a [Logmatic](http://logmatic.io/) account. - [_SqsHandler_](../src/Monolog/Handler/SqsHandler.php): Logs records to an [AWS SQS](http://docs.aws.amazon.com/aws-sdk-php/v2/guide/service-sqs.html) queue. @@ -155,6 +158,7 @@ - [_GitProcessor_](../src/Monolog/Processor/GitProcessor.php): Adds the current git branch and commit to a log record. - [_MercurialProcessor_](../src/Monolog/Processor/MercurialProcessor.php): Adds the current hg branch and commit to a log record. - [_TagProcessor_](../src/Monolog/Processor/TagProcessor.php): Adds an array of predefined tags to a log record. +- [_HostnameProcessor_](../src/Monolog/Processor/HostnameProcessor.php): Adds the current hostname to a log record. ## Third Party Packages diff --git a/doc/message-structure.md b/doc/message-structure.md index 093a10ca..63c6f19b 100644 --- a/doc/message-structure.md +++ b/doc/message-structure.md @@ -5,7 +5,7 @@ The table below describes which keys are always available for every log message. key | type | description -----------|---------------------------|------------------------------------------------------------------------------- -message | string | The log message. When the `PsrLogMessageProcessor` is used this string may contain placeholders that will be replaced by variables from the context, e.g., "User %username% logged in" with `['username' => 'John']` as context will be written as "User John logged in". +message | string | The log message. When the `PsrLogMessageProcessor` is used this string may contain placeholders that will be replaced by variables from the context, e.g., "User {username} logged in" with `['username' => 'John']` as context will be written as "User John logged in". level | int | Severity of the log message. See log levels described in [01-usage.md](01-usage.md). level_name | string | String representation of log level. context | array | Arbitrary data passed with the construction of the message. For example the username of the current user or their IP address. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 54da2818..1a676b24 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - + tests/Monolog/ diff --git a/src/Monolog/DateTimeImmutable.php b/src/Monolog/DateTimeImmutable.php index 4e9f598e..6e7a5fc6 100644 --- a/src/Monolog/DateTimeImmutable.php +++ b/src/Monolog/DateTimeImmutable.php @@ -23,29 +23,9 @@ class DateTimeImmutable extends \DateTimeImmutable implements \JsonSerializable public function __construct($useMicroseconds, \DateTimeZone $timezone = null) { - static $needsMicrosecondsHack = PHP_VERSION_ID < 70100; - $this->useMicroseconds = $useMicroseconds; - $date = 'now'; - if ($needsMicrosecondsHack && $useMicroseconds) { - $timestamp = microtime(true); - - // apply offset of the timezone as microtime() is always UTC - if ($timezone && $timezone->getName() !== 'UTC') { - $timestamp += (new \DateTime('now', $timezone))->getOffset(); - } - - // Circumvent DateTimeImmutable::createFromFormat() which always returns \DateTimeImmutable instead of `static` - // @link https://bugs.php.net/bug.php?id=60302 - // - // So we create a DateTime but then format it so we - // can re-create one using the right class - $dt = self::createFromFormat('U.u', sprintf('%.6F', $timestamp)); - $date = $dt->format('Y-m-d H:i:s.u'); - } - - parent::__construct($date, $timezone); + parent::__construct('now', $timezone); } public function jsonSerialize(): string diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index d0b1aa6d..d32c25d5 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -37,6 +37,7 @@ class ErrorHandler private $hasFatalErrorHandler; private $fatalLevel; private $reservedMemory; + private $lastFatalTrace; private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; public function __construct(LoggerInterface $logger) @@ -49,10 +50,10 @@ class ErrorHandler * * By default it will handle errors, exceptions and fatal errors * - * @param LoggerInterface $logger - * @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling - * @param array|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling - * @param int|false $fatalLevel a LogLevel::* constant, or false to disable fatal error handling + * @param LoggerInterface $logger + * @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling + * @param array|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling + * @param string|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling * @return ErrorHandler */ public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self @@ -103,7 +104,11 @@ class ErrorHandler return $this; } - public function registerFatalHandler($level = null, $reservedMemorySize = 20): self + /** + * @param string|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling + * @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 + */ + public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self { register_shutdown_function([$this, 'handleFatalError']); @@ -166,6 +171,10 @@ class ErrorHandler call_user_func($this->previousExceptionHandler, $e); } + if (!headers_sent() && ini_get('display_errors') === 0) { + http_response_code(500); + } + exit(255); } @@ -182,6 +191,10 @@ class ErrorHandler if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, 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->lastFatalTrace = $trace; } if ($this->previousErrorHandler === true) { @@ -203,7 +216,7 @@ class ErrorHandler $this->logger->log( $this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel, 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], - ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line']] + ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace] ); if ($this->logger instanceof Logger) { diff --git a/src/Monolog/Formatter/FluentdFormatter.php b/src/Monolog/Formatter/FluentdFormatter.php index a84f826a..f8df1850 100644 --- a/src/Monolog/Formatter/FluentdFormatter.php +++ b/src/Monolog/Formatter/FluentdFormatter.php @@ -62,6 +62,7 @@ class FluentdFormatter implements FormatterInterface $message = [ 'message' => $record['message'], + 'context' => $record['context'], 'extra' => $record['extra'], ]; diff --git a/src/Monolog/Formatter/HtmlFormatter.php b/src/Monolog/Formatter/HtmlFormatter.php index a343b065..26f74fa9 100644 --- a/src/Monolog/Formatter/HtmlFormatter.php +++ b/src/Monolog/Formatter/HtmlFormatter.php @@ -59,7 +59,7 @@ class HtmlFormatter extends NormalizerFormatter $td = '
'.htmlspecialchars($td, ENT_NOQUOTES, 'UTF-8').'
'; } - return "\n$th:\n".$td."\n"; + return "\n$th:\n".$td."\n"; } /** diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index 8e2f2fdd..e0d5449e 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -64,7 +64,15 @@ class JsonFormatter extends NormalizerFormatter */ public function format(array $record): string { - return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : ''); + $normalized = $this->normalize($record); + if (isset($normalized['context']) && $normalized['context'] === []) { + $normalized['context'] = new \stdClass; + } + if (isset($normalized['extra']) && $normalized['extra'] === []) { + $normalized['extra'] = new \stdClass; + } + + return $this->toJson($normalized, true) . ($this->appendNewline ? "\n" : ''); } /** @@ -122,8 +130,8 @@ class JsonFormatter extends NormalizerFormatter */ protected function normalize($data, int $depth = 0) { - if ($depth > 9) { - return 'Over 9 levels deep, aborting normalization'; + if ($depth > $this->maxNormalizeDepth) { + return 'Over '.$this->maxNormalizeDepth.' levels deep, aborting normalization'; } if (is_array($data) || $data instanceof \Traversable) { @@ -131,10 +139,11 @@ class JsonFormatter extends NormalizerFormatter $count = 1; foreach ($data as $key => $value) { - if ($count++ >= 1000) { - $normalized['...'] = 'Over 1000 items, aborting normalization'; + if ($count++ > $this->maxNormalizeItemCount) { + $normalized['...'] = 'Over '.$this->maxNormalizeItemCount.' items ('.count($data).' total), aborting normalization'; break; } + $normalized[$key] = $this->normalize($value, $depth + 1); } diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index 9d4ef1d7..6cbdb520 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -126,18 +126,14 @@ class LineFormatter extends NormalizerFormatter protected function normalizeException(\Throwable $e, int $depth = 0): string { - $previousText = ''; + $str = $this->formatException($e); + if ($previous = $e->getPrevious()) { do { - $previousText .= ', '.get_class($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); + $str .= "\n[previous exception] " . $this->formatException($previous); } while ($previous = $previous->getPrevious()); } - $str = '[object] ('.get_class($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; - if ($this->includeStacktraces) { - $str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n"; - } - return $str; } @@ -166,4 +162,14 @@ class LineFormatter extends NormalizerFormatter return str_replace(["\r\n", "\r", "\n"], ' ', $str); } + + private function formatException(\Throwable $e): string + { + $str = '[object] (' . get_class($e) . '(code: ' . $e->getCode() . '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')'; + if ($this->includeStacktraces) { + $str .= "\n[stacktrace]\n" . $e->getTraceAsString() . "\n"; + } + + return $str; + } } diff --git a/src/Monolog/Formatter/LogstashFormatter.php b/src/Monolog/Formatter/LogstashFormatter.php index 3cf31dd4..74464e57 100644 --- a/src/Monolog/Formatter/LogstashFormatter.php +++ b/src/Monolog/Formatter/LogstashFormatter.php @@ -14,8 +14,8 @@ namespace Monolog\Formatter; /** * Serializes a log message to Logstash Event Format * - * @see http://logstash.net/ - * @see https://github.com/elastic/logstash/blob/master/logstash-core-event/lib/logstash/event.rb + * @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 */ @@ -32,30 +32,30 @@ class LogstashFormatter extends NormalizerFormatter protected $applicationName; /** - * @var string a prefix for 'extra' fields from the Monolog record (optional) + * @var string the key for 'extra' fields from the Monolog record */ - protected $extraPrefix; + protected $extraKey; /** - * @var string a prefix for 'context' fields from the Monolog record (optional) + * @var string the key for 'context' fields from the Monolog record */ - protected $contextPrefix; + protected $contextKey; /** * @param string $applicationName the application that sends the data, used as the "type" field of logstash * @param string $systemName the system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine - * @param string $extraPrefix prefix for extra keys inside logstash "fields" - * @param string $contextPrefix prefix for context keys inside logstash "fields", defaults to ctxt_ + * @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 */ - public function __construct(string $applicationName, string $systemName = null, string $extraPrefix = null, string $contextPrefix = 'ctxt_') + 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 ?: gethostname(); $this->applicationName = $applicationName; - $this->extraPrefix = $extraPrefix; - $this->contextPrefix = $contextPrefix; + $this->extraKey = $extraKey; + $this->contextKey = $contextKey; } /** @@ -90,10 +90,10 @@ class LogstashFormatter extends NormalizerFormatter $message['type'] = $this->applicationName; } if (!empty($record['extra'])) { - $message[$this->extraPrefix.'extra'] = $record['extra']; + $message[$this->extraKey] = $record['extra']; } if (!empty($record['context'])) { - $message[$this->contextPrefix.'context'] = $record['context']; + $message[$this->contextKey] = $record['context']; } return $this->toJson($message) . "\n"; diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 84f644c6..2177f91e 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -24,6 +24,8 @@ class NormalizerFormatter implements FormatterInterface const SIMPLE_DATE = "Y-m-d\TH:i:sP"; protected $dateFormat; + protected $maxNormalizeDepth = 9; + protected $maxNormalizeItemCount = 1000; /** * @param string $dateFormat The format of the timestamp: one supported by DateTime::format @@ -56,14 +58,40 @@ class NormalizerFormatter implements FormatterInterface return $records; } + /** + * The maximum number of normalization levels to go through + */ + public function getMaxNormalizeDepth(): int + { + return $this->maxNormalizeDepth; + } + + public function setMaxNormalizeDepth(int $maxNormalizeDepth): void + { + $this->maxNormalizeDepth = $maxNormalizeDepth; + } + + /** + * The maximum number of items to normalize per level + */ + public function getMaxNormalizeItemCount(): int + { + return $this->maxNormalizeItemCount; + } + + public function setMaxNormalizeItemCount(int $maxNormalizeItemCount): void + { + $this->maxNormalizeItemCount = $maxNormalizeItemCount; + } + /** * @param mixed $data * @return int|bool|string|null|array */ protected function normalize($data, int $depth = 0) { - if ($depth > 9) { - return 'Over 9 levels deep, aborting normalization'; + if ($depth > $this->maxNormalizeDepth) { + return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'; } if (null === $data || is_scalar($data)) { @@ -84,10 +112,11 @@ class NormalizerFormatter implements FormatterInterface $count = 1; foreach ($data as $key => $value) { - if ($count++ >= 1000) { - $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization'; + if ($count++ > $this->maxNormalizeItemCount) { + $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization'; break; } + $normalized[$key] = $this->normalize($value, $depth + 1); } @@ -158,9 +187,20 @@ class NormalizerFormatter implements FormatterInterface if (isset($frame['file'])) { $data['trace'][] = $frame['file'].':'.$frame['line']; } elseif (isset($frame['function']) && $frame['function'] === '{closure}') { - // We should again normalize the frames, because it might contain invalid items + // Simplify closures handling $data['trace'][] = $frame['function']; } else { + if (isset($frame['args'])) { + // Make sure that objects present as arguments are not serialized nicely but rather only + // as a class name to avoid any unexpected leak of sensitive information + $frame['args'] = array_map(function ($arg) { + if (is_object($arg) && !$arg instanceof \DateTimeInterface) { + return sprintf("[object] (%s)", get_class($arg)); + } + + return $arg; + }, $frame['args']); + } // We should again normalize the frames, because it might contain invalid items $data['trace'][] = $this->toJson($this->normalize($frame, $depth + 1), true); } diff --git a/src/Monolog/Handler/AbstractHandler.php b/src/Monolog/Handler/AbstractHandler.php index ea79ec92..d4771084 100644 --- a/src/Monolog/Handler/AbstractHandler.php +++ b/src/Monolog/Handler/AbstractHandler.php @@ -24,8 +24,8 @@ abstract class AbstractHandler extends Handler protected $bubble = true; /** - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param int|string $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($level = Logger::DEBUG, $bubble = true) { @@ -67,8 +67,8 @@ abstract class AbstractHandler extends Handler /** * Sets the bubbling behavior. * - * @param Boolean $bubble true means that this handler allows bubbling. - * false means that bubbling is not permitted. + * @param bool $bubble true means that this handler allows bubbling. + * false means that bubbling is not permitted. * @return self */ public function setBubble(bool $bubble): self @@ -81,8 +81,8 @@ abstract class AbstractHandler extends Handler /** * Gets the bubbling behavior. * - * @return Boolean true means that this handler allows bubbling. - * false means that bubbling is not permitted. + * @return bool true means that this handler allows bubbling. + * false means that bubbling is not permitted. */ public function getBubble(): bool { diff --git a/src/Monolog/Handler/AbstractSyslogHandler.php b/src/Monolog/Handler/AbstractSyslogHandler.php index d6fc41ef..0c692aa9 100644 --- a/src/Monolog/Handler/AbstractSyslogHandler.php +++ b/src/Monolog/Handler/AbstractSyslogHandler.php @@ -54,9 +54,9 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler ]; /** - * @param mixed $facility - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param mixed $facility + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($facility = LOG_USER, $level = Logger::DEBUG, $bubble = true) { diff --git a/src/Monolog/Handler/BrowserConsoleHandler.php b/src/Monolog/Handler/BrowserConsoleHandler.php old mode 100644 new mode 100755 index 4879d24d..7774adef --- a/src/Monolog/Handler/BrowserConsoleHandler.php +++ b/src/Monolog/Handler/BrowserConsoleHandler.php @@ -44,11 +44,11 @@ class BrowserConsoleHandler extends AbstractProcessingHandler protected function write(array $record) { // Accumulate records - self::$records[] = $record; + static::$records[] = $record; // Register shutdown handler if not already done - if (!self::$initialized) { - self::$initialized = true; + if (!static::$initialized) { + static::$initialized = true; $this->registerShutdownFunction(); } } @@ -59,18 +59,18 @@ class BrowserConsoleHandler extends AbstractProcessingHandler */ public static function send() { - $format = self::getResponseFormat(); + $format = static::getResponseFormat(); if ($format === 'unknown') { return; } - if (count(self::$records)) { + if (count(static::$records)) { if ($format === 'html') { - self::writeOutput(''); + static::writeOutput(''); } elseif ($format === 'js') { - self::writeOutput(self::generateScript()); + static::writeOutput(static::generateScript()); } - self::reset(); + static::reset(); } } @@ -79,7 +79,7 @@ class BrowserConsoleHandler extends AbstractProcessingHandler */ public static function reset() { - self::$records = []; + static::$records = []; } /** @@ -134,18 +134,19 @@ class BrowserConsoleHandler extends AbstractProcessingHandler private static function generateScript() { $script = []; - foreach (self::$records as $record) { - $context = self::dump('Context', $record['context']); - $extra = self::dump('Extra', $record['extra']); + foreach (static::$records as $record) { + $context = static::dump('Context', $record['context']); + $extra = static::dump('Extra', $record['extra']); if (empty($context) && empty($extra)) { - $script[] = self::call_array('log', self::handleStyles($record['formatted'])); + $script[] = static::call_array('log', static::handleStyles($record['formatted'])); } else { - $script = array_merge($script, - [self::call_array('groupCollapsed', self::handleStyles($record['formatted']))], + $script = array_merge( + $script, + [static::call_array('groupCollapsed', static::handleStyles($record['formatted']))], $context, $extra, - [self::call('groupEnd')] + [static::call('groupEnd')] ); } } @@ -155,19 +156,19 @@ class BrowserConsoleHandler extends AbstractProcessingHandler private static function handleStyles($formatted) { - $args = [self::quote('font-weight: normal')]; + $args = [static::quote('font-weight: normal')]; $format = '%c' . $formatted; preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); foreach (array_reverse($matches) as $match) { - $args[] = self::quote(self::handleCustomStyles($match[2][0], $match[1][0])); + $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); $args[] = '"font-weight: normal"'; $pos = $match[0][1]; $format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0])); } - array_unshift($args, self::quote($format)); + array_unshift($args, static::quote($format)); return $args; } @@ -199,13 +200,13 @@ class BrowserConsoleHandler extends AbstractProcessingHandler if (empty($dict)) { return $script; } - $script[] = self::call('log', self::quote('%c%s'), self::quote('font-weight: bold'), self::quote($title)); + $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title)); foreach ($dict as $key => $value) { $value = json_encode($value); if (empty($value)) { - $value = self::quote(''); + $value = static::quote(''); } - $script[] = self::call('log', self::quote('%s: %o'), self::quote($key), $value); + $script[] = static::call('log', static::quote('%s: %o'), static::quote($key), $value); } return $script; @@ -221,7 +222,7 @@ class BrowserConsoleHandler extends AbstractProcessingHandler $args = func_get_args(); $method = array_shift($args); - return self::call_array($method, $args); + return static::call_array($method, $args); } private static function call_array($method, array $args) diff --git a/src/Monolog/Handler/BufferHandler.php b/src/Monolog/Handler/BufferHandler.php index 5ce6c396..676c7c14 100644 --- a/src/Monolog/Handler/BufferHandler.php +++ b/src/Monolog/Handler/BufferHandler.php @@ -36,8 +36,8 @@ class BufferHandler extends AbstractHandler implements ProcessableHandlerInterfa * @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 int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not - * @param Boolean $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @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, $bufferLimit = 0, $level = Logger::DEBUG, $bubble = true, $flushOnOverflow = false) { diff --git a/src/Monolog/Handler/ChromePHPHandler.php b/src/Monolog/Handler/ChromePHPHandler.php index 4c6be69d..9c1ca523 100644 --- a/src/Monolog/Handler/ChromePHPHandler.php +++ b/src/Monolog/Handler/ChromePHPHandler.php @@ -24,6 +24,8 @@ use Monolog\Logger; */ class ChromePHPHandler extends AbstractProcessingHandler { + use WebRequestRecognizerTrait; + /** * Version of the extension */ @@ -46,7 +48,7 @@ class ChromePHPHandler extends AbstractProcessingHandler * * Chrome limits the headers to 256KB, so when we sent 240KB we stop sending * - * @var Boolean + * @var bool */ protected static $overflowed = false; @@ -59,8 +61,8 @@ class ChromePHPHandler extends AbstractProcessingHandler protected static $sendHeaders = true; /** - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($level = Logger::DEBUG, $bubble = true) { @@ -75,6 +77,10 @@ class ChromePHPHandler extends AbstractProcessingHandler */ public function handleBatch(array $records) { + if (!$this->isWebRequest()) { + return; + } + $messages = []; foreach ($records as $record) { @@ -108,6 +114,10 @@ class ChromePHPHandler extends AbstractProcessingHandler */ protected function write(array $record) { + if (!$this->isWebRequest()) { + return; + } + self::$json['rows'][] = $record['formatted']; $this->send(); diff --git a/src/Monolog/Handler/CubeHandler.php b/src/Monolog/Handler/CubeHandler.php index ab6e007e..82b16921 100644 --- a/src/Monolog/Handler/CubeHandler.php +++ b/src/Monolog/Handler/CubeHandler.php @@ -46,7 +46,8 @@ class CubeHandler extends AbstractProcessingHandler if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) { throw new \UnexpectedValueException( 'Invalid protocol (' . $urlInfo['scheme'] . ').' - . ' Valid options are ' . implode(', ', $this->acceptedSchemes)); + . ' Valid options are ' . implode(', ', $this->acceptedSchemes) + ); } $this->scheme = $urlInfo['scheme']; diff --git a/src/Monolog/Handler/DeduplicationHandler.php b/src/Monolog/Handler/DeduplicationHandler.php index 235b3b85..6d7753cb 100644 --- a/src/Monolog/Handler/DeduplicationHandler.php +++ b/src/Monolog/Handler/DeduplicationHandler.php @@ -60,7 +60,7 @@ class DeduplicationHandler extends BufferHandler * @param string $deduplicationStore The file/path where the deduplication log should be kept * @param int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes * @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct(HandlerInterface $handler, $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, $time = 60, $bubble = true) { diff --git a/src/Monolog/Handler/ErrorLogHandler.php b/src/Monolog/Handler/ErrorLogHandler.php index 2c801398..3108aed7 100644 --- a/src/Monolog/Handler/ErrorLogHandler.php +++ b/src/Monolog/Handler/ErrorLogHandler.php @@ -29,17 +29,18 @@ class ErrorLogHandler extends AbstractProcessingHandler protected $expandNewlines; /** - * @param int $messageType Says where the error should go. - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not - * @param Boolean $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries + * @param int $messageType Says where the error should go. + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries */ public function __construct($messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, $bubble = true, $expandNewlines = false) { parent::__construct($level, $bubble); - if (false === in_array($messageType, self::getAvailableTypes())) { + 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); } @@ -73,9 +74,10 @@ class ErrorLogHandler extends AbstractProcessingHandler { if (!$this->expandNewlines) { error_log((string) $record['formatted'], $this->messageType); + return; - } - + } + $lines = preg_split('{[\r\n]+}', (string) $record['formatted']); foreach ($lines as $line) { error_log($line, $this->messageType); diff --git a/src/Monolog/Handler/FilterHandler.php b/src/Monolog/Handler/FilterHandler.php index da0634a7..b42ce966 100644 --- a/src/Monolog/Handler/FilterHandler.php +++ b/src/Monolog/Handler/FilterHandler.php @@ -42,7 +42,7 @@ class FilterHandler extends Handler implements ProcessableHandlerInterface /** * Whether the messages that are handled can bubble up the stack or not * - * @var Boolean + * @var bool */ protected $bubble; @@ -50,7 +50,7 @@ class FilterHandler extends Handler implements ProcessableHandlerInterface * @param callable|HandlerInterface $handler Handler or factory callable($record, $this). * @param int|array $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided * @param int $maxLevel Maximum level to accept, only used if $minLevelOrList is not an array - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, $bubble = true) { diff --git a/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php b/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php index f9cab618..b73854ad 100644 --- a/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php +++ b/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php @@ -21,8 +21,8 @@ interface ActivationStrategyInterface /** * Returns whether the given record activates the handler. * - * @param array $record - * @return Boolean + * @param array $record + * @return bool */ public function isHandlerActivated(array $record); } diff --git a/src/Monolog/Handler/FingersCrossedHandler.php b/src/Monolog/Handler/FingersCrossedHandler.php index f0151d1e..8943d4f0 100644 --- a/src/Monolog/Handler/FingersCrossedHandler.php +++ b/src/Monolog/Handler/FingersCrossedHandler.php @@ -43,8 +43,8 @@ class FingersCrossedHandler extends Handler implements ProcessableHandlerInterfa * @param callable|HandlerInterface $handler Handler or factory callable($record, $fingersCrossedHandler). * @param int|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action * @param int $bufferSize How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not - * @param Boolean $stopBuffering Whether the handler should stop buffering after being triggered (default true) + * @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 $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered */ public function __construct($handler, $activationStrategy = null, $bufferSize = 0, $bubble = true, $stopBuffering = true, $passthruLevel = null) diff --git a/src/Monolog/Handler/FirePHPHandler.php b/src/Monolog/Handler/FirePHPHandler.php index ac7230c7..8f5c0c69 100644 --- a/src/Monolog/Handler/FirePHPHandler.php +++ b/src/Monolog/Handler/FirePHPHandler.php @@ -21,6 +21,8 @@ use Monolog\Formatter\FormatterInterface; */ class FirePHPHandler extends AbstractProcessingHandler { + use WebRequestRecognizerTrait; + /** * WildFire JSON header message format */ @@ -130,7 +132,7 @@ class FirePHPHandler extends AbstractProcessingHandler */ protected function write(array $record) { - if (!self::$sendHeaders) { + if (!self::$sendHeaders || !$this->isWebRequest()) { return; } @@ -157,7 +159,7 @@ class FirePHPHandler extends AbstractProcessingHandler /** * Verifies if the headers are accepted by the current user agent * - * @return Boolean + * @return bool */ protected function headersAccepted() { diff --git a/src/Monolog/Handler/GelfHandler.php b/src/Monolog/Handler/GelfHandler.php index b5c3a8c8..53def822 100644 --- a/src/Monolog/Handler/GelfHandler.php +++ b/src/Monolog/Handler/GelfHandler.php @@ -41,14 +41,6 @@ class GelfHandler extends AbstractProcessingHandler $this->publisher = $publisher; } - /** - * {@inheritdoc} - */ - public function close() - { - $this->publisher = null; - } - /** * {@inheritdoc} */ diff --git a/src/Monolog/Handler/GroupHandler.php b/src/Monolog/Handler/GroupHandler.php index ef1485f4..30fd68eb 100644 --- a/src/Monolog/Handler/GroupHandler.php +++ b/src/Monolog/Handler/GroupHandler.php @@ -25,8 +25,8 @@ class GroupHandler extends Handler implements ProcessableHandlerInterface protected $handlers; /** - * @param array $handlers Array of Handlers. - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param array $handlers Array of Handlers. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct(array $handlers, $bubble = true) { diff --git a/src/Monolog/Handler/HandlerInterface.php b/src/Monolog/Handler/HandlerInterface.php index 472fd31b..8ad7e38f 100644 --- a/src/Monolog/Handler/HandlerInterface.php +++ b/src/Monolog/Handler/HandlerInterface.php @@ -29,7 +29,7 @@ interface HandlerInterface * * @param array $record Partial log record containing only a level key * - * @return Boolean + * @return bool */ public function isHandling(array $record): bool; @@ -43,9 +43,9 @@ interface HandlerInterface * Unless the bubbling is interrupted (by returning true), the Logger class will keep on * calling further handlers in the stack with a given log record. * - * @param array $record The record to handle - * @return Boolean 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. + * @param array $record The record to handle + * @return bool true means that this handler handled the record, and that bubbling is not permitted. + * false means the record was either not processed or that this handler allows bubbling. */ public function handle(array $record): bool; diff --git a/src/Monolog/Handler/HipChatHandler.php b/src/Monolog/Handler/HipChatHandler.php index 98441f3e..87286e3f 100644 --- a/src/Monolog/Handler/HipChatHandler.php +++ b/src/Monolog/Handler/HipChatHandler.php @@ -188,6 +188,21 @@ class HipChatHandler extends SocketHandler protected function write(array $record) { parent::write($record); + $this->finalizeWrite(); + } + + /** + * Finalizes the request by reading some bytes and then closing the socket + * + * If we do not read some but close the socket too early, hipchat sometimes + * drops the request entirely. + */ + protected function finalizeWrite() + { + $res = $this->getResource(); + if (is_resource($res)) { + @fread($res, 2048); + } $this->closeSocket(); } diff --git a/src/Monolog/Handler/IFTTTHandler.php b/src/Monolog/Handler/IFTTTHandler.php index 46792ee4..f5d440df 100644 --- a/src/Monolog/Handler/IFTTTHandler.php +++ b/src/Monolog/Handler/IFTTTHandler.php @@ -30,10 +30,10 @@ class IFTTTHandler extends AbstractProcessingHandler private $secretKey; /** - * @param string $eventName The name of the IFTTT Maker event that should be triggered - * @param string $secretKey A valid IFTTT secret key - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $eventName The name of the IFTTT Maker event that should be triggered + * @param string $secretKey A valid IFTTT secret key + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($eventName, $secretKey, $level = Logger::ERROR, $bubble = true) { diff --git a/src/Monolog/Handler/InsightOpsHandler.php b/src/Monolog/Handler/InsightOpsHandler.php new file mode 100644 index 00000000..bd6dfe60 --- /dev/null +++ b/src/Monolog/Handler/InsightOpsHandler.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; + + /** + * Inspired on LogEntriesHandler. + * + * @author Robert Kaufmann III + * @author Gabriel Machado + */ +class InsightOpsHandler extends SocketHandler +{ + /** + * @var string + */ + protected $logToken; + + /** + * @param string $token Log token supplied by InsightOps + * @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'. + * @param bool $useSSL Whether or not SSL encryption should be used + * @param int $level The minimum logging level to trigger this handler + * @param bool $bubble Whether or not messages that are handled should bubble up the stack. + * + * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing + */ + public function __construct($token, $region = 'us', $useSSL = true, $level = Logger::DEBUG, $bubble = true) + { + if ($useSSL && !extension_loaded('openssl')) { + throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler'); + } + + $endpoint = $useSSL + ? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443' + : $region . '.data.logs.insight.rapid7.com:80'; + + parent::__construct($endpoint, $level, $bubble); + $this->logToken = $token; + } + + /** + * {@inheritdoc} + * + * @param array $record + * @return string + */ + protected function generateDataStream($record) + { + return $this->logToken . ' ' . $record['formatted']; + } +} diff --git a/src/Monolog/Handler/MandrillHandler.php b/src/Monolog/Handler/MandrillHandler.php index 066c649e..6cf90e7f 100644 --- a/src/Monolog/Handler/MandrillHandler.php +++ b/src/Monolog/Handler/MandrillHandler.php @@ -27,7 +27,7 @@ class MandrillHandler extends MailHandler * @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 * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($apiKey, $message, $level = Logger::ERROR, $bubble = true) { diff --git a/src/Monolog/Handler/MongoDBHandler.php b/src/Monolog/Handler/MongoDBHandler.php index f302d312..5415ee52 100644 --- a/src/Monolog/Handler/MongoDBHandler.php +++ b/src/Monolog/Handler/MongoDBHandler.php @@ -44,7 +44,7 @@ class MongoDBHandler extends AbstractProcessingHandler * @param string $database Database name * @param string $collection Collection name * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($mongodb, $database, $collection, $level = Logger::DEBUG, $bubble = true) { diff --git a/src/Monolog/Handler/NewRelicHandler.php b/src/Monolog/Handler/NewRelicHandler.php index fb037782..d757067b 100644 --- a/src/Monolog/Handler/NewRelicHandler.php +++ b/src/Monolog/Handler/NewRelicHandler.php @@ -19,6 +19,8 @@ use Monolog\Formatter\FormatterInterface; * Class to record a log on a NewRelic application. * Enabling New Relic High Security mode may prevent capture of useful information. * + * This handler requires a NormalizerFormatter to function and expects an array in $record['formatted'] + * * @see https://docs.newrelic.com/docs/agents/php-agent * @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security */ @@ -85,7 +87,7 @@ class NewRelicHandler extends AbstractProcessingHandler unset($record['formatted']['context']['transaction_name']); } - if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) { + if (isset($record['context']['exception']) && ($record['context']['exception'] instanceof \Exception || (PHP_VERSION_ID >= 70000 && $record['context']['exception'] instanceof \Throwable))) { newrelic_notice_error($record['message'], $record['context']['exception']); unset($record['formatted']['context']['exception']); } else { diff --git a/src/Monolog/Handler/PsrHandler.php b/src/Monolog/Handler/PsrHandler.php index 415b45f0..e39233b4 100644 --- a/src/Monolog/Handler/PsrHandler.php +++ b/src/Monolog/Handler/PsrHandler.php @@ -31,7 +31,7 @@ class PsrHandler extends AbstractHandler /** * @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, $bubble = true) { diff --git a/src/Monolog/Handler/PushoverHandler.php b/src/Monolog/Handler/PushoverHandler.php index b53ce6c1..3e683422 100644 --- a/src/Monolog/Handler/PushoverHandler.php +++ b/src/Monolog/Handler/PushoverHandler.php @@ -69,8 +69,8 @@ class PushoverHandler extends SocketHandler * @param string|array $users Pushover user id or array of ids the message will be sent to * @param string $title Title sent to the Pushover API * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not - * @param Boolean $useSSL Whether to connect via SSL. Required when pushing messages to users that are not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @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 $highPriorityLevel The minimum logging level at which this handler will start * sending "high priority" requests to the Pushover API @@ -180,6 +180,6 @@ class PushoverHandler extends SocketHandler */ public function useFormattedMessage($value) { - $this->useFormattedMessage = (boolean) $value; + $this->useFormattedMessage = (bool) $value; } } diff --git a/src/Monolog/Handler/RavenHandler.php b/src/Monolog/Handler/RavenHandler.php index 1c8e7185..7ea5fd7d 100644 --- a/src/Monolog/Handler/RavenHandler.php +++ b/src/Monolog/Handler/RavenHandler.php @@ -57,7 +57,7 @@ class RavenHandler extends AbstractProcessingHandler /** * @param Raven_Client $ravenClient * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct(Raven_Client $ravenClient, $level = Logger::DEBUG, $bubble = true) { @@ -181,7 +181,7 @@ class RavenHandler extends AbstractProcessingHandler } if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { - $options['extra']['message'] = $record['formatted']; + $options['message'] = $record['formatted']; $this->ravenClient->captureException($record['context']['exception'], $options); } else { $this->ravenClient->captureMessage($record['formatted'], [], $options); diff --git a/src/Monolog/Handler/RollbarHandler.php b/src/Monolog/Handler/RollbarHandler.php index 5b6678e5..2f35093c 100644 --- a/src/Monolog/Handler/RollbarHandler.php +++ b/src/Monolog/Handler/RollbarHandler.php @@ -11,7 +11,7 @@ namespace Monolog\Handler; -use RollbarNotifier; +use Rollbar\RollbarLogger; use Throwable; use Monolog\Logger; @@ -19,7 +19,7 @@ use Monolog\Logger; * Sends errors to Rollbar * * If the context data contains a `payload` key, that is used as an array - * of payload options to RollbarNotifier's report_message/report_exception methods. + * 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: @@ -34,11 +34,9 @@ use Monolog\Logger; class RollbarHandler extends AbstractProcessingHandler { /** - * Rollbar notifier - * - * @var RollbarNotifier + * @var RollbarLogger */ - protected $rollbarNotifier; + protected $rollbarLogger; protected $levelMap = [ Logger::DEBUG => 'debug', @@ -61,13 +59,13 @@ class RollbarHandler extends AbstractProcessingHandler protected $initialized = false; /** - * @param RollbarNotifier $rollbarNotifier RollbarNotifier object constructed with valid token - * @param int $level The minimum logging level at which this handler will be triggered - * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ - public function __construct(RollbarNotifier $rollbarNotifier, $level = Logger::ERROR, $bubble = true) + public function __construct(RollbarLogger $rollbarLogger, $level = Logger::ERROR, $bubble = true) { - $this->rollbarNotifier = $rollbarNotifier; + $this->rollbarLogger = $rollbarLogger; parent::__construct($level, $bubble); } @@ -84,11 +82,6 @@ class RollbarHandler extends AbstractProcessingHandler } $context = $record['context']; - $payload = []; - if (isset($context['payload'])) { - $payload = $context['payload']; - unset($context['payload']); - } $context = array_merge($context, $record['extra'], [ 'level' => $this->levelMap[$record['level']], 'monolog_level' => $record['level_name'], @@ -97,27 +90,22 @@ class RollbarHandler extends AbstractProcessingHandler ]); if (isset($context['exception']) && $context['exception'] instanceof Throwable) { - $payload['level'] = $context['level']; $exception = $context['exception']; unset($context['exception']); - - $this->rollbarNotifier->report_exception($exception, $context, $payload); + $toLog = $exception; } else { - $this->rollbarNotifier->report_message( - $record['message'], - $context['level'], - $context, - $payload - ); + $toLog = $record['message']; } + $this->rollbarLogger->log($context['level'], $toLog, $context); + $this->hasRecords = true; } public function flush() { if ($this->hasRecords) { - $this->rollbarNotifier->flush(); + $this->rollbarLogger->flush(); $this->hasRecords = false; } } diff --git a/src/Monolog/Handler/RotatingFileHandler.php b/src/Monolog/Handler/RotatingFileHandler.php index a4a5ba44..d9733ba0 100644 --- a/src/Monolog/Handler/RotatingFileHandler.php +++ b/src/Monolog/Handler/RotatingFileHandler.php @@ -40,9 +40,9 @@ class RotatingFileHandler extends StreamHandler * @param string $filename * @param int $maxFiles The maximal amount of files to keep (0 means unlimited) * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) - * @param Boolean $useLocking Try to lock log file before doing any writes + * @param bool $useLocking Try to lock log file before doing any writes */ public function __construct($filename, $maxFiles = 0, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false) { @@ -50,7 +50,7 @@ class RotatingFileHandler extends StreamHandler $this->maxFiles = (int) $maxFiles; $this->nextRotation = new \DateTimeImmutable('tomorrow'); $this->filenameFormat = '{filename}-{date}'; - $this->dateFormat = 'Y-m-d'; + $this->dateFormat = self::FILE_PER_DAY; parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking); } @@ -98,7 +98,7 @@ class RotatingFileHandler extends StreamHandler $this->mustRotate = !file_exists($this->url); } - if ($this->nextRotation < $record['datetime']) { + if ($this->nextRotation <= $record['datetime']) { $this->mustRotate = true; $this->close(); } @@ -166,7 +166,7 @@ class RotatingFileHandler extends StreamHandler $fileInfo = pathinfo($this->filename); $glob = str_replace( ['{filename}', '{date}'], - [$fileInfo['filename'], '*'], + [$fileInfo['filename'], '[0-9][0-9][0-9][0-9]*'], $fileInfo['dirname'] . '/' . $this->filenameFormat ); if (!empty($fileInfo['extension'])) { diff --git a/src/Monolog/Handler/Slack/SlackRecord.php b/src/Monolog/Handler/Slack/SlackRecord.php old mode 100644 new mode 100755 index d9e6a4cb..90286e88 --- a/src/Monolog/Handler/Slack/SlackRecord.php +++ b/src/Monolog/Handler/Slack/SlackRecord.php @@ -145,7 +145,7 @@ class SlackRecord if ($this->useShortAttachment) { $attachment['fields'][] = $this->generateAttachmentField( - ucfirst($key), + $key, $record[$key] ); } else { @@ -211,8 +211,8 @@ class SlackRecord $hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric')); return $hasSecondDimension || $hasNonNumericKeys - ? json_encode($normalized, $prettyPrintFlag) - : json_encode($normalized); + ? json_encode($normalized, $prettyPrintFlag|JSON_UNESCAPED_UNICODE) + : json_encode($normalized, JSON_UNESCAPED_UNICODE); } /** @@ -229,7 +229,7 @@ class SlackRecord * Generates attachment field * * @param string $title - * @param string|array $value\ + * @param string|array $value * * @return array */ @@ -240,7 +240,7 @@ class SlackRecord : $value; return array( - 'title' => $title, + 'title' => ucfirst($title), 'value' => $value, 'short' => false, ); @@ -256,7 +256,7 @@ class SlackRecord private function generateAttachmentFields(array $data) { $fields = array(); - foreach ($data as $key => $value) { + foreach ($this->normalizerFormatter->format($data) as $key => $value) { $fields[] = $this->generateAttachmentField($key, $value); } diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index a99872f9..6f671ac1 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -63,8 +63,7 @@ class SlackHandler extends SocketHandler $iconEmoji, $useShortAttachment, $includeContextAndExtra, - $excludeFields, - $this->formatter + $excludeFields ); $this->token = $token; @@ -75,6 +74,11 @@ class SlackHandler extends SocketHandler return $this->slackRecord; } + public function getToken() + { + return $this->token; + } + /** * {@inheritdoc} * diff --git a/src/Monolog/Handler/SlackWebhookHandler.php b/src/Monolog/Handler/SlackWebhookHandler.php index a2ea52ac..2904db3f 100644 --- a/src/Monolog/Handler/SlackWebhookHandler.php +++ b/src/Monolog/Handler/SlackWebhookHandler.php @@ -60,8 +60,7 @@ class SlackWebhookHandler extends AbstractProcessingHandler $iconEmoji, $useShortAttachment, $includeContextAndExtra, - $excludeFields, - $this->formatter + $excludeFields ); } @@ -70,6 +69,11 @@ class SlackWebhookHandler extends AbstractProcessingHandler return $this->slackRecord; } + public function getWebhookUrl() + { + return $this->webhookUrl; + } + /** * {@inheritdoc} * @@ -86,7 +90,7 @@ class SlackWebhookHandler extends AbstractProcessingHandler CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array('Content-type: application/json'), - CURLOPT_POSTFIELDS => $postString + CURLOPT_POSTFIELDS => $postString, ); if (defined('CURLOPT_SAFE_UPLOAD')) { $options[CURLOPT_SAFE_UPLOAD] = true; diff --git a/src/Monolog/Handler/SocketHandler.php b/src/Monolog/Handler/SocketHandler.php index e2161ff1..36ea0b5f 100644 --- a/src/Monolog/Handler/SocketHandler.php +++ b/src/Monolog/Handler/SocketHandler.php @@ -30,15 +30,16 @@ class SocketHandler extends AbstractProcessingHandler /** @var float */ private $writingTimeout = 10; private $lastSentBytes = null; + private $chunkSize = null; private $persistent = false; private $errno; private $errstr; private $lastWritingAt; /** - * @param string $connectionString Socket connection string - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $connectionString Socket connection string + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true) { @@ -90,7 +91,7 @@ class SocketHandler extends AbstractProcessingHandler */ public function setPersistent($persistent) { - $this->persistent = (boolean) $persistent; + $this->persistent = (bool) $persistent; } /** @@ -130,6 +131,16 @@ class SocketHandler extends AbstractProcessingHandler $this->writingTimeout = (float) $seconds; } + /** + * Set chunk size. Only has effect during connection in the writing cycle. + * + * @param float $bytes + */ + public function setChunkSize($bytes) + { + $this->chunkSize = $bytes; + } + /** * Get current connection string * @@ -176,6 +187,16 @@ class SocketHandler extends AbstractProcessingHandler return $this->writingTimeout; } + /** + * Get current chunk size + * + * @return float + */ + public function getChunkSize() + { + return $this->chunkSize; + } + /** * Check to see if the socket is currently available. * @@ -218,6 +239,16 @@ class SocketHandler extends AbstractProcessingHandler 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 + */ + protected function streamSetChunkSize() + { + return stream_set_chunk_size($this->resource, $this->chunkSize); + } + /** * Wrapper to allow mocking */ @@ -267,6 +298,7 @@ class SocketHandler extends AbstractProcessingHandler { $this->createSocketResource(); $this->setSocketTimeout(); + $this->setStreamChunkSize(); } private function createSocketResource() @@ -289,6 +321,13 @@ class SocketHandler extends AbstractProcessingHandler } } + private function setStreamChunkSize() + { + if ($this->chunkSize && !$this->streamSetChunkSize()) { + throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()"); + } + } + private function writeToSocket($data) { $length = strlen($data); diff --git a/src/Monolog/Handler/SqsHandler.php b/src/Monolog/Handler/SqsHandler.php index 01c70dce..ed595c71 100644 --- a/src/Monolog/Handler/SqsHandler.php +++ b/src/Monolog/Handler/SqsHandler.php @@ -21,6 +21,11 @@ use Monolog\Logger; */ class SqsHandler extends AbstractProcessingHandler { + /** 256 KB in bytes - maximum message size in SQS */ + const MAX_MESSAGE_SIZE = 262144; + /** 100 KB in bytes - head message size for new error log */ + const HEAD_MESSAGE_SIZE = 102400; + /** @var SqsClient */ private $client; /** @var string */ @@ -45,9 +50,14 @@ class SqsHandler extends AbstractProcessingHandler throw new \InvalidArgumentException('SqsHandler accepts only formatted records as a string'); } + $messageBody = $record['formatted']; + if (strlen($messageBody) >= self::MAX_MESSAGE_SIZE) { + $messageBody = substr($messageBody, 0, self::HEAD_MESSAGE_SIZE); + } + $this->client->sendMessage([ 'QueueUrl' => $this->queueUrl, - 'MessageBody' => $record['formatted'], + 'MessageBody' => $messageBody, ]); } } diff --git a/src/Monolog/Handler/StreamHandler.php b/src/Monolog/Handler/StreamHandler.php index 5abbc592..6631ef4e 100644 --- a/src/Monolog/Handler/StreamHandler.php +++ b/src/Monolog/Handler/StreamHandler.php @@ -34,9 +34,9 @@ class StreamHandler extends AbstractProcessingHandler /** * @param resource|string $stream * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) - * @param Boolean $useLocking Try to lock log file before doing any writes + * @param bool $useLocking Try to lock log file before doing any writes * * @throws \Exception If a missing directory is not buildable * @throws \InvalidArgumentException If stream is not a resource or string @@ -106,6 +106,7 @@ class StreamHandler extends AbstractProcessingHandler restore_error_handler(); if (!is_resource($this->stream)) { $this->stream = null; + throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: '.$this->errorMessage, $this->url)); } } @@ -169,7 +170,7 @@ class StreamHandler extends AbstractProcessingHandler set_error_handler([$this, 'customErrorHandler']); $status = mkdir($dir, 0777, true); restore_error_handler(); - if (false === $status) { + if (false === $status && !is_dir($dir)) { throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and its not buildable: '.$this->errorMessage, $dir)); } } diff --git a/src/Monolog/Handler/SwiftMailerHandler.php b/src/Monolog/Handler/SwiftMailerHandler.php index 3359ac8f..73ef3cfc 100644 --- a/src/Monolog/Handler/SwiftMailerHandler.php +++ b/src/Monolog/Handler/SwiftMailerHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Logger; +use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Swift_Message; use Swift; @@ -48,6 +49,16 @@ class SwiftMailerHandler extends MailHandler $this->mailer->send($this->buildMessage($content, $records)); } + /** + * Gets the formatter for the Swift_Message subject. + * + * @param string $format The format of the subject + */ + protected function getSubjectFormatter(string $format): FormatterInterface + { + return new LineFormatter($format); + } + /** * Creates instance of Swift_Message to be sent * @@ -70,7 +81,7 @@ class SwiftMailerHandler extends MailHandler } if ($records) { - $subjectFormatter = new LineFormatter($message->getSubject()); + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); $message->setSubject($subjectFormatter->format($this->getHighestRecord($records))); } diff --git a/src/Monolog/Handler/SyslogHandler.php b/src/Monolog/Handler/SyslogHandler.php index 863fc56d..df7c89a8 100644 --- a/src/Monolog/Handler/SyslogHandler.php +++ b/src/Monolog/Handler/SyslogHandler.php @@ -32,11 +32,11 @@ class SyslogHandler extends AbstractSyslogHandler protected $logopts; /** - * @param string $ident - * @param mixed $facility - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not - * @param int $logopts Option flags for the openlog() call, defaults to LOG_PID + * @param string $ident + * @param mixed $facility + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param int $logopts Option flags for the openlog() call, defaults to LOG_PID */ public function __construct($ident, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $logopts = LOG_PID) { diff --git a/src/Monolog/Handler/SyslogUdpHandler.php b/src/Monolog/Handler/SyslogUdpHandler.php index 2258f56e..dc844020 100644 --- a/src/Monolog/Handler/SyslogUdpHandler.php +++ b/src/Monolog/Handler/SyslogUdpHandler.php @@ -25,12 +25,12 @@ class SyslogUdpHandler extends AbstractSyslogHandler protected $ident; /** - * @param string $host - * @param int $port - * @param mixed $facility - * @param int $level The minimum logging level at which this handler will be triggered - * @param Boolean $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 string $host + * @param int $port + * @param mixed $facility + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $ident Program name or tag for each log message. */ public function __construct($host, $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $ident = 'php') { diff --git a/src/Monolog/Handler/TestHandler.php b/src/Monolog/Handler/TestHandler.php index 3df7b6da..55c48334 100644 --- a/src/Monolog/Handler/TestHandler.php +++ b/src/Monolog/Handler/TestHandler.php @@ -84,14 +84,25 @@ class TestHandler extends AbstractProcessingHandler return isset($this->recordsByLevel[$level]); } + /** + * @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records + * @param int $level Logger::LEVEL constant value + */ public function hasRecord($record, $level) { - if (is_array($record)) { - $record = $record['message']; + if (is_string($record)) { + $record = array('message' => $record); } return $this->hasRecordThatPasses(function ($rec) use ($record) { - return $rec['message'] === $record; + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + + return true; }, $level); } diff --git a/src/Monolog/Handler/WebRequestRecognizerTrait.php b/src/Monolog/Handler/WebRequestRecognizerTrait.php new file mode 100644 index 00000000..c8183528 --- /dev/null +++ b/src/Monolog/Handler/WebRequestRecognizerTrait.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +trait WebRequestRecognizerTrait +{ + /** + * Checks if PHP's serving a web request + * @return bool + */ + protected function isWebRequest(): bool + { + return 'cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI; + } +} diff --git a/src/Monolog/Handler/WhatFailureGroupHandler.php b/src/Monolog/Handler/WhatFailureGroupHandler.php index 42c2336a..c2a58d5d 100644 --- a/src/Monolog/Handler/WhatFailureGroupHandler.php +++ b/src/Monolog/Handler/WhatFailureGroupHandler.php @@ -46,6 +46,16 @@ class WhatFailureGroupHandler extends GroupHandler */ public function handleBatch(array $records) { + if ($this->processors) { + $processed = array(); + foreach ($records as $record) { + foreach ($this->processors as $processor) { + $processed[] = call_user_func($processor, $record); + } + } + $records = $processed; + } + foreach ($this->handlers as $handler) { try { $handler->handleBatch($records); diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php index 88ecf0dc..a22bf7ec 100644 --- a/src/Monolog/Logger.php +++ b/src/Monolog/Logger.php @@ -15,6 +15,7 @@ use DateTimeZone; use Monolog\Handler\HandlerInterface; use Psr\Log\LoggerInterface; use Psr\Log\InvalidArgumentException; +use Throwable; /** * Monolog log channel @@ -87,8 +88,6 @@ class Logger implements LoggerInterface const API = 2; /** - * Logging levels from syslog protocol defined in RFC 5424 - * * This is a static variable and not a constant to serve as an extension point for custom levels * * @var string[] $levels Logging levels with the levels as key @@ -135,6 +134,11 @@ class Logger implements LoggerInterface */ protected $timezone; + /** + * @var callable + */ + protected $exceptionHandler; + /** * @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. @@ -144,7 +148,7 @@ class Logger implements LoggerInterface public function __construct(string $name, array $handlers = [], array $processors = [], DateTimeZone $timezone = null) { $this->name = $name; - $this->handlers = $handlers; + $this->setHandlers($handlers); $this->processors = $processors; $this->timezone = $timezone ?: new DateTimeZone(date_default_timezone_get() ?: 'UTC'); } @@ -272,10 +276,10 @@ class Logger implements LoggerInterface /** * Adds a log record. * - * @param int $level The logging level - * @param string $message The log message - * @param array $context The log context - * @return Boolean Whether the record has been processed + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + * @return bool Whether the record has been processed */ public function addRecord(int $level, string $message, array $context = []): bool { @@ -304,22 +308,26 @@ class Logger implements LoggerInterface 'extra' => [], ]; - foreach ($this->processors as $processor) { - $record = call_user_func($processor, $record); - } - - // advance the array pointer to the first handler that will handle this record - reset($this->handlers); - while ($handlerKey !== key($this->handlers)) { - next($this->handlers); - } - - while ($handler = current($this->handlers)) { - if (true === $handler->handle($record)) { - break; + try { + foreach ($this->processors as $processor) { + $record = call_user_func($processor, $record); } - next($this->handlers); + // advance the array pointer to the first handler that will handle this record + reset($this->handlers); + while ($handlerKey !== key($this->handlers)) { + next($this->handlers); + } + + while ($handler = current($this->handlers)) { + if (true === $handler->handle($record)) { + break; + } + + next($this->handlers); + } + } catch (Throwable $e) { + $this->handleException($e, $record); } return true; @@ -338,9 +346,7 @@ class Logger implements LoggerInterface /** * Gets the name of the logging level. * - * @param int $level * @throws \Psr\Log\InvalidArgumentException If level is not defined - * @return string */ public static function getLevelName(int $level): string { @@ -356,7 +362,6 @@ class Logger implements LoggerInterface * * @param string|int Level number (monolog) or name (PSR-3) * @throws \Psr\Log\InvalidArgumentException If level is not defined - * @return int */ public static function toMonologLevel($level): int { @@ -373,9 +378,6 @@ class Logger implements LoggerInterface /** * Checks whether the Logger has a handler that listens on the given level - * - * @param int $level - * @return Boolean */ public function isHandling(int $level): bool { @@ -392,6 +394,23 @@ class Logger implements LoggerInterface return false; } + /** + * Set a custom exception handler that will be called if adding a new record fails + * + * The callable will receive an exception object and the record that failed to be logged + */ + public function setExceptionHandler(?callable $callback): self + { + $this->exceptionHandler = $callback; + + return $this; + } + + public function getExceptionHandler(): ?callable + { + return $this->exceptionHandler; + } + /** * Adds a log record at an arbitrary level. * @@ -513,9 +532,7 @@ class Logger implements LoggerInterface } /** - * Set the timezone to be used for the timestamp of log records. - * - * @param DateTimeZone $tz Timezone object + * Sets the timezone to be used for the timestamp of log records. */ public function setTimezone(DateTimeZone $tz): self { @@ -525,12 +542,23 @@ class Logger implements LoggerInterface } /** - * Set the timezone to be used for the timestamp of log records. - * - * @return DateTimeZone + * 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, array $record) + { + if (!$this->exceptionHandler) { + throw $e; + } + + call_user_func($this->exceptionHandler, $e, $record); + } } diff --git a/src/Monolog/Processor/HostnameProcessor.php b/src/Monolog/Processor/HostnameProcessor.php new file mode 100644 index 00000000..fef40849 --- /dev/null +++ b/src/Monolog/Processor/HostnameProcessor.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +/** + * Injects value of gethostname in all records + */ +class HostnameProcessor +{ + private static $host; + + public function __construct() + { + self::$host = (string) gethostname(); + } + + public function __invoke(array $record): array + { + $record['extra']['hostname'] = self::$host; + + return $record; + } +} diff --git a/src/Monolog/Processor/IntrospectionProcessor.php b/src/Monolog/Processor/IntrospectionProcessor.php index 3a275293..5fe5b2aa 100644 --- a/src/Monolog/Processor/IntrospectionProcessor.php +++ b/src/Monolog/Processor/IntrospectionProcessor.php @@ -51,12 +51,7 @@ class IntrospectionProcessor return $record; } - /* - * http://php.net/manual/en/function.debug-backtrace.php - * As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added. - * Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'. - */ - $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS); + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // skip first since it's always the current method array_shift($trace); @@ -70,11 +65,13 @@ class IntrospectionProcessor foreach ($this->skipClassesPartials as $part) { if (strpos($trace[$i]['class'], $part) !== false) { $i++; + continue 2; } } } elseif (in_array($trace[$i]['function'], $this->skipFunctions)) { $i++; + continue; } diff --git a/src/Monolog/Processor/PsrLogMessageProcessor.php b/src/Monolog/Processor/PsrLogMessageProcessor.php index 80784038..7311c2be 100644 --- a/src/Monolog/Processor/PsrLogMessageProcessor.php +++ b/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -20,16 +20,21 @@ namespace Monolog\Processor; */ class PsrLogMessageProcessor { - const SIMPLE_DATE = "Y-m-d\TH:i:sP"; + const SIMPLE_DATE = "Y-m-d\TH:i:s.uP"; private $dateFormat; + /** @var bool */ + private $removeUsedContextFields; + /** - * @param string $dateFormat The format of the timestamp: one supported by DateTime::format + * @param string $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) + public function __construct(string $dateFormat = null, bool $removeUsedContextFields = false) { - $this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat; + $this->dateFormat = $dateFormat; + $this->removeUsedContextFields = $removeUsedContextFields; } /** @@ -44,14 +49,31 @@ class PsrLogMessageProcessor $replacements = []; foreach ($record['context'] as $key => $val) { + $placeholder = '{' . $key . '}'; + if (strpos($record['message'], $placeholder) === false) { + continue; + } + if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { - $replacements['{'.$key.'}'] = $val; + $replacements[$placeholder] = $val; } elseif ($val instanceof \DateTimeInterface) { - $replacements['{'.$key.'}'] = $val->format($this->dateFormat); + if (!$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 (is_object($val)) { - $replacements['{'.$key.'}'] = '[object '.get_class($val).']'; + $replacements[$placeholder] = '[object '.get_class($val).']'; + } elseif (is_array($val)) { + $replacements[$placeholder] = 'array'.@json_encode($val); } else { - $replacements['{'.$key.'}'] = '['.gettype($val).']'; + $replacements[$placeholder] = '['.gettype($val).']'; + } + + if ($this->removeUsedContextFields) { + unset($record['context'][$key]); } } diff --git a/tests/Monolog/Formatter/FluentdFormatterTest.php b/tests/Monolog/Formatter/FluentdFormatterTest.php index 0be8d8f9..ca6ba3bb 100644 --- a/tests/Monolog/Formatter/FluentdFormatterTest.php +++ b/tests/Monolog/Formatter/FluentdFormatterTest.php @@ -40,7 +40,7 @@ class FluentdFormatterTest extends TestCase $formatter = new FluentdFormatter(); $this->assertEquals( - '["test",0,{"message":"test","extra":[],"level":300,"level_name":"WARNING"}]', + '["test",0,{"message":"test","context":[],"extra":[],"level":300,"level_name":"WARNING"}]', $formatter->format($record) ); } @@ -55,7 +55,7 @@ class FluentdFormatterTest extends TestCase $formatter = new FluentdFormatter(true); $this->assertEquals( - '["test.error",0,{"message":"test","extra":[]}]', + '["test.error",0,{"message":"test","context":[],"extra":[]}]', $formatter->format($record) ); } diff --git a/tests/Monolog/Formatter/GelfMessageFormatterTest.php b/tests/Monolog/Formatter/GelfMessageFormatterTest.php index 39eeaf70..012de3cd 100644 --- a/tests/Monolog/Formatter/GelfMessageFormatterTest.php +++ b/tests/Monolog/Formatter/GelfMessageFormatterTest.php @@ -234,7 +234,7 @@ class GelfMessageFormatterTest extends \PHPUnit\Framework\TestCase 'context' => array('exception' => str_repeat(' ', 32767 * 2)), 'datetime' => new \DateTime("@0"), 'extra' => array('key' => str_repeat(' ', 32767 * 2)), - 'message' => 'log' + 'message' => 'log', ); $message = $formatter->format($record); $messageArray = $message->toArray(); diff --git a/tests/Monolog/Formatter/JsonFormatterTest.php b/tests/Monolog/Formatter/JsonFormatterTest.php index 6dfdbb7a..7ccf5bc8 100644 --- a/tests/Monolog/Formatter/JsonFormatterTest.php +++ b/tests/Monolog/Formatter/JsonFormatterTest.php @@ -38,11 +38,12 @@ class JsonFormatterTest extends TestCase { $formatter = new JsonFormatter(); $record = $this->getRecord(); + $record['context'] = $record['extra'] = new \stdClass; $this->assertEquals(json_encode($record)."\n", $formatter->format($record)); $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); $record = $this->getRecord(); - $this->assertEquals('{"message":"test","context":[],"level":300,"level_name":"WARNING","channel":"test","datetime":"'.$record['datetime']->format('Y-m-d\TH:i:s.uP').'","extra":[]}', $formatter->format($record)); + $this->assertEquals('{"message":"test","context":{},"level":300,"level_name":"WARNING","channel":"test","datetime":"'.$record['datetime']->format('Y-m-d\TH:i:s.uP').'","extra":{}}', $formatter->format($record)); } /** @@ -71,6 +72,7 @@ class JsonFormatterTest extends TestCase $this->getRecord(Logger::DEBUG), ]; array_walk($expected, function (&$value, $key) { + $value['context'] = $value['extra'] = new \stdClass; $value = json_encode($value); }); $this->assertEquals(implode("\n", $expected), $formatter->formatBatch($records)); @@ -110,6 +112,47 @@ class JsonFormatterTest extends TestCase $this->assertContextContainsFormattedException($formattedThrowable, $message); } + public function testMaxNormalizeDepth() + { + $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true); + $formatter->setMaxNormalizeDepth(1); + $throwable = new \Error('Foo'); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + + $this->assertContextContainsFormattedException('"Over 1 levels deep, aborting normalization"', $message); + } + + public function testMaxNormalizeItemCountWith0ItemsMax() + { + $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true); + $formatter->setMaxNormalizeDepth(9); + $formatter->setMaxNormalizeItemCount(0); + $throwable = new \Error('Foo'); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + + $this->assertEquals( + '{"...":"Over 0 items (6 total), aborting normalization"}'."\n", + $message + ); + } + + public function testMaxNormalizeItemCountWith2ItemsMax() + { + $formatter = new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, true); + $formatter->setMaxNormalizeDepth(9); + $formatter->setMaxNormalizeItemCount(2); + $throwable = new \Error('Foo'); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + + $this->assertEquals( + '{"level_name":"CRITICAL","channel":"core","...":"Over 2 items (6 total), aborting normalization"}'."\n", + $message + ); + } + /** * @param string $expected * @param string $actual @@ -119,18 +162,18 @@ class JsonFormatterTest extends TestCase private function assertContextContainsFormattedException($expected, $actual) { $this->assertEquals( - '{"level_name":"CRITICAL","channel":"core","context":{"exception":'.$expected.'},"datetime":null,"extra":[],"message":"foobar"}'."\n", + '{"level_name":"CRITICAL","channel":"core","context":{"exception":'.$expected.'},"datetime":null,"extra":{},"message":"foobar"}'."\n", $actual ); } /** - * @param JsonFormatter $formatter - * @param \Exception|\Throwable $exception + * @param JsonFormatter $formatter + * @param \Throwable $exception * * @return string */ - private function formatRecordWithExceptionInContext(JsonFormatter $formatter, $exception) + private function formatRecordWithExceptionInContext(JsonFormatter $formatter, \Throwable $exception) { $message = $formatter->format([ 'level_name' => 'CRITICAL', @@ -176,4 +219,40 @@ class JsonFormatterTest extends TestCase return $formattedException; } + + public function testNormalizeHandleLargeArraysWithExactly1000Items() + { + $formatter = new NormalizerFormatter(); + $largeArray = range(1, 1000); + + $res = $formatter->format(array( + 'level_name' => 'CRITICAL', + 'channel' => 'test', + 'message' => 'bar', + 'context' => array($largeArray), + 'datetime' => new \DateTime, + 'extra' => array(), + )); + + $this->assertCount(1000, $res['context'][0]); + $this->assertArrayNotHasKey('...', $res['context'][0]); + } + + public function testNormalizeHandleLargeArrays() + { + $formatter = new NormalizerFormatter(); + $largeArray = range(1, 2000); + + $res = $formatter->format(array( + 'level_name' => 'CRITICAL', + 'channel' => 'test', + 'message' => 'bar', + 'context' => array($largeArray), + 'datetime' => new \DateTime, + 'extra' => array(), + )); + + $this->assertCount(1001, $res['context'][0]); + $this->assertEquals('Over 1000 items (2000 total), aborting normalization', $res['context'][0]['...']); + } } diff --git a/tests/Monolog/Formatter/LineFormatterTest.php b/tests/Monolog/Formatter/LineFormatterTest.php index a8b55e44..e19cde1e 100644 --- a/tests/Monolog/Formatter/LineFormatterTest.php +++ b/tests/Monolog/Formatter/LineFormatterTest.php @@ -170,7 +170,7 @@ class LineFormatterTest extends \PHPUnit\Framework\TestCase $path = str_replace('\\/', '/', json_encode(__FILE__)); - $this->assertEquals('['.date('Y-m-d').'] core.CRITICAL: foobar {"exception":"[object] (RuntimeException(code: 0): Foo at '.substr($path, 1, -1).':'.(__LINE__ - 8).', LogicException(code: 0): Wut? at '.substr($path, 1, -1).':'.(__LINE__ - 12).')"} []'."\n", $message); + $this->assertEquals('['.date('Y-m-d').'] core.CRITICAL: foobar {"exception":"[object] (RuntimeException(code: 0): Foo at '.substr($path, 1, -1).':'.(__LINE__ - 8).')\n[previous exception] [object] (LogicException(code: 0): Wut? at '.substr($path, 1, -1).':'.(__LINE__ - 12).')"} []'."\n", $message); } public function testBatchFormat() diff --git a/tests/Monolog/Formatter/LogstashFormatterTest.php b/tests/Monolog/Formatter/LogstashFormatterTest.php index 2ca9f55e..5d0374bc 100644 --- a/tests/Monolog/Formatter/LogstashFormatterTest.php +++ b/tests/Monolog/Formatter/LogstashFormatterTest.php @@ -17,7 +17,7 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase { public function tearDown() { - \PHPUnit_Framework_Error_Warning::$enabled = true; + \PHPUnit\Framework\Error\Warning::$enabled = true; return parent::tearDown(); } @@ -27,7 +27,7 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase */ public function testDefaultFormatterV1() { - $formatter = new LogstashFormatter('test', 'hostname', null, 'ctxt_'); + $formatter = new LogstashFormatter('test', 'hostname'); $record = [ 'level' => Logger::ERROR, 'level_name' => 'ERROR', @@ -49,7 +49,7 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase $this->assertEquals('test', $message['type']); $this->assertEquals('hostname', $message['host']); - $formatter = new LogstashFormatter('mysystem', null, null, 'ctxt_'); + $formatter = new LogstashFormatter('mysystem'); $message = json_decode($formatter->format($record), true); @@ -61,7 +61,7 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase */ public function testFormatWithFileAndLineV1() { - $formatter = new LogstashFormatter('test', null, null, 'ctxt_'); + $formatter = new LogstashFormatter('test'); $record = [ 'level' => Logger::ERROR, 'level_name' => 'ERROR', @@ -83,7 +83,7 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase */ public function testFormatWithContextV1() { - $formatter = new LogstashFormatter('test', null, null, 'ctxt_'); + $formatter = new LogstashFormatter('test'); $record = [ 'level' => Logger::ERROR, 'level_name' => 'ERROR', @@ -96,17 +96,17 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase $message = json_decode($formatter->format($record), true); - $this->assertArrayHasKey('ctxt_context', $message); - $this->assertArrayHasKey('from', $message['ctxt_context']); - $this->assertEquals('logger', $message['ctxt_context']['from']); + $this->assertArrayHasKey('context', $message); + $this->assertArrayHasKey('from', $message['context']); + $this->assertEquals('logger', $message['context']['from']); // Test with extraPrefix - $formatter = new LogstashFormatter('test', null, null, 'CTX'); + $formatter = new LogstashFormatter('test', null, 'extra', 'CTX'); $message = json_decode($formatter->format($record), true); - $this->assertArrayHasKey('CTXcontext', $message); - $this->assertArrayHasKey('from', $message['CTXcontext']); - $this->assertEquals('logger', $message['CTXcontext']['from']); + $this->assertArrayHasKey('CTX', $message); + $this->assertArrayHasKey('from', $message['CTX']); + $this->assertEquals('logger', $message['CTX']['from']); } /** @@ -114,7 +114,7 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase */ public function testFormatWithExtraV1() { - $formatter = new LogstashFormatter('test', null, null, 'ctxt_'); + $formatter = new LogstashFormatter('test'); $record = [ 'level' => Logger::ERROR, 'level_name' => 'ERROR', @@ -132,17 +132,17 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase $this->assertEquals('pair', $message['extra']['key']); // Test with extraPrefix - $formatter = new LogstashFormatter('test', null, 'EXT', 'ctxt_'); + $formatter = new LogstashFormatter('test', null, 'EXTRA'); $message = json_decode($formatter->format($record), true); - $this->assertArrayHasKey('EXTextra', $message); - $this->assertArrayHasKey('key', $message['EXTextra']); - $this->assertEquals('pair', $message['EXTextra']['key']); + $this->assertArrayHasKey('EXTRA', $message); + $this->assertArrayHasKey('key', $message['EXTRA']); + $this->assertEquals('pair', $message['EXTRA']['key']); } public function testFormatWithApplicationNameV1() { - $formatter = new LogstashFormatter('app', 'test', null, 'ctxt_'); + $formatter = new LogstashFormatter('app', 'test'); $record = [ 'level' => Logger::ERROR, 'level_name' => 'ERROR', diff --git a/tests/Monolog/Formatter/NormalizerFormatterTest.php b/tests/Monolog/Formatter/NormalizerFormatterTest.php index 68b275df..01bf36d2 100644 --- a/tests/Monolog/Formatter/NormalizerFormatterTest.php +++ b/tests/Monolog/Formatter/NormalizerFormatterTest.php @@ -18,7 +18,7 @@ class NormalizerFormatterTest extends \PHPUnit\Framework\TestCase { public function tearDown() { - \PHPUnit_Framework_Error_Warning::$enabled = true; + \PHPUnit\Framework\Error\Warning::$enabled = true; return parent::tearDown(); } @@ -227,6 +227,24 @@ class NormalizerFormatterTest extends \PHPUnit\Framework\TestCase $this->assertEquals(@json_encode([$resource]), $res); } + public function testNormalizeHandleLargeArraysWithExactly1000Items() + { + $formatter = new NormalizerFormatter(); + $largeArray = range(1, 1000); + + $res = $formatter->format(array( + 'level_name' => 'CRITICAL', + 'channel' => 'test', + 'message' => 'bar', + 'context' => array($largeArray), + 'datetime' => new \DateTime, + 'extra' => array(), + )); + + $this->assertCount(1000, $res['context'][0]); + $this->assertArrayNotHasKey('...', $res['context'][0]); + } + public function testNormalizeHandleLargeArrays() { $formatter = new NormalizerFormatter(); @@ -241,7 +259,7 @@ class NormalizerFormatterTest extends \PHPUnit\Framework\TestCase 'extra' => array(), )); - $this->assertCount(1000, $res['context'][0]); + $this->assertCount(1001, $res['context'][0]); $this->assertEquals('Over 1000 items (2000 total), aborting normalization', $res['context'][0]['...']); } @@ -271,6 +289,48 @@ class NormalizerFormatterTest extends \PHPUnit\Framework\TestCase $this->assertSame('{"message":"€ŠšŽžŒœŸ"}', $res); } + public function testMaxNormalizeDepth() + { + $formatter = new NormalizerFormatter(); + $formatter->setMaxNormalizeDepth(1); + $throwable = new \Error('Foo'); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + $this->assertEquals( + 'Over 1 levels deep, aborting normalization', + $message['context']['exception'] + ); + } + + public function testMaxNormalizeItemCountWith0ItemsMax() + { + $formatter = new NormalizerFormatter(); + $formatter->setMaxNormalizeDepth(9); + $formatter->setMaxNormalizeItemCount(0); + $throwable = new \Error('Foo'); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + $this->assertEquals( + ["..." => "Over 0 items (6 total), aborting normalization"], + $message + ); + } + + public function testMaxNormalizeItemCountWith3ItemsMax() + { + $formatter = new NormalizerFormatter(); + $formatter->setMaxNormalizeDepth(9); + $formatter->setMaxNormalizeItemCount(2); + $throwable = new \Error('Foo'); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + + $this->assertEquals( + ["level_name" => "CRITICAL", "channel" => "core", "..." => "Over 2 items (6 total), aborting normalization"], + $message + ); + } + /** * @param mixed $in Input * @param mixed $expect Expected output @@ -333,10 +393,6 @@ class NormalizerFormatterTest extends \PHPUnit\Framework\TestCase // and no file or line are included in the trace because it's treated as internal function public function testExceptionTraceWithArgs() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported in HHVM since it detects errors differently'); - } - try { // This will contain $resource and $wrappedResource as arguments in the trace item $resource = fopen('php://memory', 'rw+'); @@ -356,19 +412,54 @@ class NormalizerFormatterTest extends \PHPUnit\Framework\TestCase $record = ['context' => ['exception' => $e]]; $result = $formatter->format($record); - $this->assertRegExp( - '%\[resource\(stream\)\]%', + $this->assertSame( + '{"function":"Monolog\\\\Formatter\\\\{closure}","class":"Monolog\\\\Formatter\\\\NormalizerFormatterTest","type":"->","args":["[object] (Monolog\\\\Formatter\\\\TestFooNorm)","[resource(stream)]"]}', $result['context']['exception']['trace'][0] ); + } - $pattern = '%\[\{"Monolog\\\\\\\\Formatter\\\\\\\\TestFooNorm":"JSON_ERROR"\}%'; + /** + * @param NormalizerFormatter $formatter + * @param \Throwable $exception + * + * @return string + */ + private function formatRecordWithExceptionInContext(NormalizerFormatter $formatter, \Throwable $exception) + { + $message = $formatter->format([ + 'level_name' => 'CRITICAL', + 'channel' => 'core', + 'context' => ['exception' => $exception], + 'datetime' => null, + 'extra' => [], + 'message' => 'foobar', + ]); - // Tests that the wrapped resource is ignored while encoding, only works for PHP <= 5.4 - $this->assertRegExp( - $pattern, + return $message; + } + + public function testExceptionTraceDoesNotLeakCallUserFuncArgs() + { + try { + $arg = new TestInfoLeak; + call_user_func(array($this, 'throwHelper'), $arg, $dt = new \DateTime()); + } catch (\Exception $e) { + } + + $formatter = new NormalizerFormatter(); + $record = array('context' => array('exception' => $e)); + $result = $formatter->format($record); + + $this->assertSame( + '{"function":"throwHelper","class":"Monolog\\\\Formatter\\\\NormalizerFormatterTest","type":"->","args":["[object] (Monolog\\\\Formatter\\\\TestInfoLeak)","'.$dt->format('Y-m-d\TH:i:sP').'"]}', $result['context']['exception']['trace'][0] ); } + + private function throwHelper($arg) + { + throw new \RuntimeException('Thrown'); + } } class TestFooNorm @@ -410,3 +501,11 @@ class TestToStringError throw new \RuntimeException('Could not convert to string'); } } + +class TestInfoLeak +{ + public function __toString() + { + return 'Sensitive information'; + } +} diff --git a/tests/Monolog/Handler/ChromePHPHandlerTest.php b/tests/Monolog/Handler/ChromePHPHandlerTest.php index e9a1f982..7168ce49 100644 --- a/tests/Monolog/Handler/ChromePHPHandlerTest.php +++ b/tests/Monolog/Handler/ChromePHPHandlerTest.php @@ -153,4 +153,9 @@ class TestChromePHPHandler extends ChromePHPHandler { return $this->headers; } + + protected function isWebRequest(): bool + { + return true; + } } diff --git a/tests/Monolog/Handler/ElasticSearchHandlerTest.php b/tests/Monolog/Handler/ElasticSearchHandlerTest.php index e7d02297..b3503b06 100644 --- a/tests/Monolog/Handler/ElasticSearchHandlerTest.php +++ b/tests/Monolog/Handler/ElasticSearchHandlerTest.php @@ -201,6 +201,7 @@ class ElasticSearchHandlerTest extends TestCase ->setHosts($hosts) ->build(); $handler = new ElasticSearchHandler($client, $this->options); + try { $handler->handleBatch([$msg]); } catch (\RuntimeException $e) { diff --git a/tests/Monolog/Handler/ErrorLogHandlerTest.php b/tests/Monolog/Handler/ErrorLogHandlerTest.php index d230fb1f..16d8e47d 100644 --- a/tests/Monolog/Handler/ErrorLogHandlerTest.php +++ b/tests/Monolog/Handler/ErrorLogHandlerTest.php @@ -32,7 +32,7 @@ class ErrorLogHandlerTest extends TestCase * @expectedException InvalidArgumentException * @expectedExceptionMessage The given message type "42" is not supported */ - public function testShouldNotAcceptAnInvalidTypeOnContructor() + public function testShouldNotAcceptAnInvalidTypeOnConstructor() { new ErrorLogHandler(42); } diff --git a/tests/Monolog/Handler/FilterHandlerTest.php b/tests/Monolog/Handler/FilterHandlerTest.php index 0e64e769..b0072de0 100644 --- a/tests/Monolog/Handler/FilterHandlerTest.php +++ b/tests/Monolog/Handler/FilterHandlerTest.php @@ -146,7 +146,10 @@ class FilterHandlerTest extends TestCase $handler = new FilterHandler( function ($record, $handler) use ($test) { return $test; - }, Logger::INFO, Logger::NOTICE, false + }, + Logger::INFO, + Logger::NOTICE, + false ); $handler->handle($this->getRecord(Logger::DEBUG)); $handler->handle($this->getRecord(Logger::INFO)); diff --git a/tests/Monolog/Handler/FirePHPHandlerTest.php b/tests/Monolog/Handler/FirePHPHandlerTest.php index b62e7fd0..07df2fe8 100644 --- a/tests/Monolog/Handler/FirePHPHandlerTest.php +++ b/tests/Monolog/Handler/FirePHPHandlerTest.php @@ -93,4 +93,9 @@ class TestFirePHPHandler extends FirePHPHandler { return $this->headers; } + + protected function isWebRequest(): bool + { + return true; + } } diff --git a/tests/Monolog/Handler/InsightOpsHandlerTest.php b/tests/Monolog/Handler/InsightOpsHandlerTest.php new file mode 100644 index 00000000..209858ae --- /dev/null +++ b/tests/Monolog/Handler/InsightOpsHandlerTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Test\TestCase; + use Monolog\Logger; + + /** + * @author Robert Kaufmann III + * @author Gabriel Machado + */ +class InsightOpsHandlerTest extends TestCase +{ + /** + * @var resource + */ + private $resource; + + /** + * @var LogEntriesHandler + */ + private $handler; + + public function testWriteContent() + { + $this->createHandler(); + $this->handler->handle($this->getRecord(Logger::CRITICAL, 'Critical write test')); + + fseek($this->resource, 0); + $content = fread($this->resource, 1024); + + $this->assertRegexp('/testToken \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+00:00\] test.CRITICAL: Critical write test/', $content); + } + + public function testWriteBatchContent() + { + $this->createHandler(); + $this->handler->handleBatch($this->getMultipleRecords()); + + fseek($this->resource, 0); + $content = fread($this->resource, 1024); + + $this->assertRegexp('/(testToken \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+00:00\] .* \[\] \[\]\n){3}/', $content); + } + + private function createHandler() + { + $useSSL = extension_loaded('openssl'); + $args = array('testToken', 'us', $useSSL, Logger::DEBUG, true); + $this->resource = fopen('php://memory', 'a'); + $this->handler = $this->getMockBuilder(InsightOpsHandler::class) + ->setMethods(array('fsockopen', 'streamSetTimeout', 'closeSocket')) + ->setConstructorArgs($args) + ->getMock(); + + $reflectionProperty = new \ReflectionProperty('\Monolog\Handler\SocketHandler', 'connectionString'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->handler, 'localhost:1234'); + + $this->handler->expects($this->any()) + ->method('fsockopen') + ->will($this->returnValue($this->resource)); + $this->handler->expects($this->any()) + ->method('streamSetTimeout') + ->will($this->returnValue(true)); + $this->handler->expects($this->any()) + ->method('closeSocket') + ->will($this->returnValue(true)); + } +} diff --git a/tests/Monolog/Handler/LogmaticHandlerTest.php b/tests/Monolog/Handler/LogmaticHandlerTest.php index bab74ac9..de33c6bb 100644 --- a/tests/Monolog/Handler/LogmaticHandlerTest.php +++ b/tests/Monolog/Handler/LogmaticHandlerTest.php @@ -37,7 +37,7 @@ class LogmaticHandlerTest extends TestCase fseek($this->res, 0); $content = fread($this->res, 1024); - $this->assertRegexp('/testToken {"message":"Critical write test","context":\[\],"level":500,"level_name":"CRITICAL","channel":"test","datetime":"(.*)","extra":\[\],"hostname":"testHostname","appname":"testAppname","@marker":\["sourcecode","php"\]}/', $content); + $this->assertRegexp('/testToken {"message":"Critical write test","context":{},"level":500,"level_name":"CRITICAL","channel":"test","datetime":"(.*)","extra":{},"hostname":"testHostname","appname":"testAppname","@marker":\["sourcecode","php"\]}/', $content); } public function testWriteBatchContent() @@ -53,7 +53,7 @@ class LogmaticHandlerTest extends TestCase fseek($this->res, 0); $content = fread($this->res, 1024); - $this->assertRegexp('/testToken {"message":"test","context":\[\],"level":300,"level_name":"WARNING","channel":"test","datetime":"(.*)","extra":\[\],"hostname":"testHostname","appname":"testAppname","@marker":\["sourcecode","php"\]}/', $content); + $this->assertRegexp('/testToken {"message":"test","context":{},"level":300,"level_name":"WARNING","channel":"test","datetime":"(.*)","extra":{},"hostname":"testHostname","appname":"testAppname","@marker":\["sourcecode","php"\]}/', $content); } private function createHandler() diff --git a/tests/Monolog/Handler/ProcessHandlerTest.php b/tests/Monolog/Handler/ProcessHandlerTest.php index dc2a427f..c78d5590 100644 --- a/tests/Monolog/Handler/ProcessHandlerTest.php +++ b/tests/Monolog/Handler/ProcessHandlerTest.php @@ -77,7 +77,7 @@ class ProcessHandlerTest extends TestCase */ public function testConstructWithInvalidCommandThrowsInvalidArgumentException($invalidCommand, $expectedExcep) { - $this->setExpectedException($expectedExcep); + $this->expectException($expectedExcep); new ProcessHandler($invalidCommand, Logger::DEBUG); } @@ -102,7 +102,7 @@ class ProcessHandlerTest extends TestCase */ public function testConstructWithInvalidCwdThrowsInvalidArgumentException($invalidCwd, $expectedExcep) { - $this->setExpectedException($expectedExcep); + $this->expectException($expectedExcep); new ProcessHandler(self::DUMMY_COMMAND, Logger::DEBUG, true, $invalidCwd); } @@ -135,7 +135,7 @@ class ProcessHandlerTest extends TestCase ->method('selectErrorStream') ->will($this->returnValue(false)); - $this->setExpectedException('\UnexpectedValueException'); + $this->expectException('\UnexpectedValueException'); /** @var ProcessHandler $handler */ $handler->handle($this->getRecord(Logger::WARNING, 'stream failing, whoops')); } @@ -147,7 +147,7 @@ class ProcessHandlerTest extends TestCase public function testStartupWithErrorsThrowsUnexpectedValueException() { $handler = new ProcessHandler('>&2 echo "some fake error message"'); - $this->setExpectedException('\UnexpectedValueException'); + $this->expectException('\UnexpectedValueException'); $handler->handle($this->getRecord(Logger::WARNING, 'some warning in the house')); } @@ -167,7 +167,7 @@ class ProcessHandlerTest extends TestCase ->method('readProcessErrors') ->willReturnOnConsecutiveCalls('', $this->returnValue('some fake error message here')); - $this->setExpectedException('\UnexpectedValueException'); + $this->expectException('\UnexpectedValueException'); /** @var ProcessHandler $handler */ $handler->handle($this->getRecord(Logger::WARNING, 'some test stuff')); } diff --git a/tests/Monolog/Handler/RollbarHandlerTest.php b/tests/Monolog/Handler/RollbarHandlerTest.php index 89fc9cb9..67a0eb68 100644 --- a/tests/Monolog/Handler/RollbarHandlerTest.php +++ b/tests/Monolog/Handler/RollbarHandlerTest.php @@ -15,6 +15,7 @@ use Exception; use Monolog\Test\TestCase; use Monolog\Logger; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Rollbar\RollbarLogger; /** * @author Erik Johansson @@ -27,7 +28,7 @@ class RollbarHandlerTest extends TestCase /** * @var MockObject */ - private $rollbarNotifier; + private $rollbarLogger; /** * @var array @@ -38,7 +39,7 @@ class RollbarHandlerTest extends TestCase { parent::setUp(); - $this->setupRollbarNotifierMock(); + $this->setupRollbarLoggerMock(); } /** @@ -54,15 +55,21 @@ class RollbarHandlerTest extends TestCase $this->assertEquals('debug', $this->reportedExceptionArguments['payload']['level']); } - private function setupRollbarNotifierMock() + private function setupRollbarLoggerMock() { - $this->rollbarNotifier = $this->getMockBuilder('RollbarNotifier') - ->setMethods(array('report_message', 'report_exception', 'flush')) + $config = array( + 'access_token' => 'ad865e76e7fb496fab096ac07b1dbabb', + 'environment' => 'test', + ); + + $this->rollbarLogger = $this->getMockBuilder(RollbarLogger::class) + ->setConstructorArgs(array($config)) + ->setMethods(array('log')) ->getMock(); - $this->rollbarNotifier + $this->rollbarLogger ->expects($this->any()) - ->method('report_exception') + ->method('log') ->willReturnCallback(function ($exception, $context, $payload) { $this->reportedExceptionArguments = compact('exception', 'context', 'payload'); }); @@ -70,7 +77,7 @@ class RollbarHandlerTest extends TestCase private function createHandler(): RollbarHandler { - return new RollbarHandler($this->rollbarNotifier, Logger::DEBUG); + return new RollbarHandler($this->rollbarLogger, Logger::DEBUG); } private function createExceptionRecord($level = Logger::DEBUG, $message = 'test', $exception = null): array diff --git a/tests/Monolog/Handler/RotatingFileHandlerTest.php b/tests/Monolog/Handler/RotatingFileHandlerTest.php index f2d61db9..7d3e8c4f 100644 --- a/tests/Monolog/Handler/RotatingFileHandlerTest.php +++ b/tests/Monolog/Handler/RotatingFileHandlerTest.php @@ -102,10 +102,10 @@ class RotatingFileHandlerTest extends TestCase $dayCallback = function ($ago) use ($now) { return $now + 86400 * $ago; }; - $monthCallback = function($ago) { + $monthCallback = function ($ago) { return gmmktime(0, 0, 0, (int) (date('n') + $ago), 1, (int) date('Y')); }; - $yearCallback = function($ago) { + $yearCallback = function ($ago) { return gmmktime(0, 0, 0, 1, 1, (int) (date('Y') + $ago)); }; @@ -134,7 +134,8 @@ class RotatingFileHandlerTest extends TestCase { $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2); if (!$valid) { - $this->setExpectedExceptionRegExp(InvalidArgumentException::class, '~^Invalid date format~'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageRegExp('~^Invalid date format~'); } $handler->setFilenameFormat('{filename}-{date}', $dateFormat); $this->assertTrue(true); @@ -174,7 +175,8 @@ class RotatingFileHandlerTest extends TestCase { $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2); if (!$valid) { - $this->setExpectedExceptionRegExp(InvalidArgumentException::class, '~^Invalid filename format~'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageRegExp('~^Invalid filename format~'); } $handler->setFilenameFormat($filenameFormat, RotatingFileHandler::FILE_PER_DAY); @@ -193,6 +195,39 @@ class RotatingFileHandlerTest extends TestCase ]; } + /** + * @dataProvider rotationWhenSimilarFilesExistTests + */ + public function testRotationWhenSimilarFileNamesExist($dateFormat) + { + touch($old1 = __DIR__.'/Fixtures/foo-foo-'.date($dateFormat).'.rot'); + touch($old2 = __DIR__.'/Fixtures/foo-bar-'.date($dateFormat).'.rot'); + + $log = __DIR__.'/Fixtures/foo-'.date($dateFormat).'.rot'; + + $handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2); + $handler->setFormatter($this->getIdentityFormatter()); + $handler->setFilenameFormat('{filename}-{date}', $dateFormat); + $handler->handle($this->getRecord()); + $handler->close(); + + $this->assertTrue(file_exists($log)); + } + + public function rotationWhenSimilarFilesExistTests() + { + return array( + 'Rotation is triggered when the file of the current day is not present but similar exists' + => array(RotatingFileHandler::FILE_PER_DAY), + + 'Rotation is triggered when the file of the current month is not present but similar exists' + => array(RotatingFileHandler::FILE_PER_MONTH), + + 'Rotation is triggered when the file of the current year is not present but similar exists' + => array(RotatingFileHandler::FILE_PER_YEAR), + ); + } + public function testReuseCurrentFile() { $log = __DIR__.'/Fixtures/foo-'.date('Y-m-d').'.rot'; diff --git a/tests/Monolog/Handler/Slack/SlackRecordTest.php b/tests/Monolog/Handler/Slack/SlackRecordTest.php index aa5787f7..eaa15571 100644 --- a/tests/Monolog/Handler/Slack/SlackRecordTest.php +++ b/tests/Monolog/Handler/Slack/SlackRecordTest.php @@ -324,12 +324,12 @@ class SlackRecordTest extends TestCase 'short' => false, ), array( - 'title' => 'tags', + 'title' => 'Tags', 'value' => sprintf('```%s```', json_encode($extra['tags'])), 'short' => false, ), array( - 'title' => 'test', + 'title' => 'Test', 'value' => $context['test'], 'short' => false, ), @@ -357,6 +357,14 @@ class SlackRecordTest extends TestCase $this->assertSame($record['datetime']->getTimestamp(), $attachment['ts']); } + public function testContextHasException() + { + $record = $this->getRecord(Logger::CRITICAL, 'This is a critical message.', array('exception' => new \Exception())); + $slackRecord = new SlackRecord(null, null, true, null, false, true); + $data = $slackRecord->getSlackData($record); + $this->assertInternalType('string', $data['attachments'][0]['fields'][1]['value']); + } + public function testExcludeExtraAndContextFields() { $record = $this->getRecord( @@ -372,12 +380,12 @@ class SlackRecordTest extends TestCase $expected = array( array( - 'title' => 'info', + 'title' => 'Info', 'value' => sprintf('```%s```', json_encode(array('author' => 'Jordi'), $this->jsonPrettyPrintFlag)), 'short' => false, ), array( - 'title' => 'tags', + 'title' => 'Tags', 'value' => sprintf('```%s```', json_encode(array('web'))), 'short' => false, ), diff --git a/tests/Monolog/Handler/SocketHandlerTest.php b/tests/Monolog/Handler/SocketHandlerTest.php index c0080e84..5f76048c 100644 --- a/tests/Monolog/Handler/SocketHandlerTest.php +++ b/tests/Monolog/Handler/SocketHandlerTest.php @@ -77,6 +77,13 @@ class SocketHandlerTest extends TestCase $this->assertEquals(10.25, $this->handler->getWritingTimeout()); } + public function testSetChunkSize() + { + $this->createHandler('localhost:1234'); + $this->handler->setChunkSize(1025); + $this->assertEquals(1025, $this->handler->getChunkSize()); + } + public function testSetConnectionString() { $this->createHandler('tcp://localhost:9090'); @@ -120,6 +127,19 @@ class SocketHandlerTest extends TestCase $this->writeRecord('Hello world'); } + /** + * @expectedException UnexpectedValueException + */ + public function testExceptionIsThrownIfCannotSetChunkSize() + { + $this->setMockHandler(array('streamSetChunkSize')); + $this->handler->setChunkSize(8192); + $this->handler->expects($this->once()) + ->method('streamSetChunkSize') + ->will($this->returnValue(false)); + $this->writeRecord('Hello world'); + } + /** * @expectedException RuntimeException */ @@ -277,7 +297,7 @@ class SocketHandlerTest extends TestCase { $this->res = fopen('php://memory', 'a'); - $defaultMethods = ['fsockopen', 'pfsockopen', 'streamSetTimeout']; + $defaultMethods = ['fsockopen', 'pfsockopen', 'streamSetTimeout', 'streamSetChunkSize']; $newMethods = array_diff($methods, $defaultMethods); $finalMethods = array_merge($defaultMethods, $newMethods); @@ -305,6 +325,12 @@ class SocketHandlerTest extends TestCase ->will($this->returnValue(true)); } + if (!in_array('streamSetChunkSize', $methods)) { + $this->handler->expects($this->any()) + ->method('streamSetChunkSize') + ->will($this->returnValue(8192)); + } + $this->handler->setFormatter($this->getIdentityFormatter()); } } diff --git a/tests/Monolog/Handler/TestHandlerTest.php b/tests/Monolog/Handler/TestHandlerTest.php index db3f01ff..cc8e60ff 100644 --- a/tests/Monolog/Handler/TestHandlerTest.php +++ b/tests/Monolog/Handler/TestHandlerTest.php @@ -54,6 +54,54 @@ class TestHandlerTest extends TestCase $this->assertEquals([$record], $records); } + public function testHandlerAssertEmptyContext() + { + $handler = new TestHandler; + $record = $this->getRecord(Logger::WARNING, 'test', []); + $this->assertFalse($handler->hasWarning([ + 'message' => 'test', + 'context' => [], + ])); + + $handler->handle($record); + + $this->assertTrue($handler->hasWarning([ + 'message' => 'test', + 'context' => [], + ])); + $this->assertFalse($handler->hasWarning([ + 'message' => 'test', + 'context' => [ + 'foo' => 'bar', + ], + ])); + } + + public function testHandlerAssertNonEmptyContext() + { + $handler = new TestHandler; + $record = $this->getRecord(Logger::WARNING, 'test', ['foo' => 'bar']); + $this->assertFalse($handler->hasWarning([ + 'message' => 'test', + 'context' => [ + 'foo' => 'bar', + ], + ])); + + $handler->handle($record); + + $this->assertTrue($handler->hasWarning([ + 'message' => 'test', + 'context' => [ + 'foo' => 'bar', + ], + ])); + $this->assertFalse($handler->hasWarning([ + 'message' => 'test', + 'context' => [], + ])); + } + public function methodProvider() { return [ diff --git a/tests/Monolog/Handler/WhatFailureGroupHandlerTest.php b/tests/Monolog/Handler/WhatFailureGroupHandlerTest.php index 60f51755..475889e2 100644 --- a/tests/Monolog/Handler/WhatFailureGroupHandlerTest.php +++ b/tests/Monolog/Handler/WhatFailureGroupHandlerTest.php @@ -87,6 +87,29 @@ class WhatFailureGroupHandlerTest extends TestCase $this->assertTrue($records[0]['extra']['foo']); } + /** + * @covers Monolog\Handler\WhatFailureGroupHandler::handleBatch + */ + public function testHandleBatchUsesProcessors() + { + $testHandlers = array(new TestHandler(), new TestHandler()); + $handler = new WhatFailureGroupHandler($testHandlers); + $handler->pushProcessor(function ($record) { + $record['extra']['foo'] = true; + + return $record; + }); + $handler->handleBatch(array($this->getRecord(Logger::DEBUG), $this->getRecord(Logger::INFO))); + foreach ($testHandlers as $test) { + $this->assertTrue($test->hasDebugRecords()); + $this->assertTrue($test->hasInfoRecords()); + $this->assertTrue(count($test->getRecords()) === 2); + $records = $test->getRecords(); + $this->assertTrue($records[0]['extra']['foo']); + $this->assertTrue($records[1]['extra']['foo']); + } + } + /** * @covers Monolog\Handler\WhatFailureGroupHandler::handle */ diff --git a/tests/Monolog/LoggerTest.php b/tests/Monolog/LoggerTest.php index 428daa0a..69ea4010 100644 --- a/tests/Monolog/LoggerTest.php +++ b/tests/Monolog/LoggerTest.php @@ -576,4 +576,63 @@ class LoggerTest extends \PHPUnit\Framework\TestCase 'without microseconds' => [false, PHP_VERSION_ID >= 70100 ? 'assertNotSame' : 'assertSame', 'Y-m-d\TH:i:sP'], ]; } + + /** + * @covers Monolog\Logger::setExceptionHandler + */ + public function testSetExceptionHandler() + { + $logger = new Logger(__METHOD__); + $this->assertNull($logger->getExceptionHandler()); + $callback = function ($ex) { + }; + $logger->setExceptionHandler($callback); + $this->assertEquals($callback, $logger->getExceptionHandler()); + } + + /** + * @covers Monolog\Logger::handleException + * @expectedException Exception + */ + public function testDefaultHandleException() + { + $logger = new Logger(__METHOD__); + $handler = $this->getMockBuilder('Monolog\Handler\HandlerInterface')->getMock(); + $handler->expects($this->any()) + ->method('isHandling') + ->will($this->returnValue(true)) + ; + $handler->expects($this->any()) + ->method('handle') + ->will($this->throwException(new \Exception('Some handler exception'))) + ; + $logger->pushHandler($handler); + $logger->info('test'); + } + + /** + * @covers Monolog\Logger::handleException + * @covers Monolog\Logger::addRecord + */ + public function testCustomHandleException() + { + $logger = new Logger(__METHOD__); + $that = $this; + $logger->setExceptionHandler(function ($e, $record) use ($that) { + $that->assertEquals($e->getMessage(), 'Some handler exception'); + $that->assertTrue(is_array($record)); + $that->assertEquals($record['message'], 'test'); + }); + $handler = $this->getMockBuilder('Monolog\Handler\HandlerInterface')->getMock(); + $handler->expects($this->any()) + ->method('isHandling') + ->will($this->returnValue(true)) + ; + $handler->expects($this->any()) + ->method('handle') + ->will($this->throwException(new \Exception('Some handler exception'))) + ; + $logger->pushHandler($handler); + $logger->info('test'); + } } diff --git a/tests/Monolog/Processor/HostnameProcessorTest.php b/tests/Monolog/Processor/HostnameProcessorTest.php new file mode 100644 index 00000000..1659851b --- /dev/null +++ b/tests/Monolog/Processor/HostnameProcessorTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Processor; + +use Monolog\Test\TestCase; + +class HostnameProcessorTest extends TestCase +{ + /** + * @covers Monolog\Processor\HostnameProcessor::__invoke + */ + public function testProcessor() + { + $processor = new HostnameProcessor(); + $record = $processor($this->getRecord()); + $this->assertArrayHasKey('hostname', $record['extra']); + $this->assertInternalType('string', $record['extra']['hostname']); + $this->assertNotEmpty($record['extra']['hostname']); + $this->assertEquals(gethostname(), $record['extra']['hostname']); + } +} diff --git a/tests/Monolog/Processor/PsrLogMessageProcessorTest.php b/tests/Monolog/Processor/PsrLogMessageProcessorTest.php index 5071d892..3b5ec6e9 100644 --- a/tests/Monolog/Processor/PsrLogMessageProcessorTest.php +++ b/tests/Monolog/Processor/PsrLogMessageProcessorTest.php @@ -25,6 +25,19 @@ class PsrLogMessageProcessorTest extends \PHPUnit\Framework\TestCase 'context' => ['foo' => $val], ]); $this->assertEquals($expected, $message['message']); + $this->assertSame(['foo' => $val], $message['context']); + } + + public function testReplacementWithContextRemoval() + { + $proc = new PsrLogMessageProcessor($dateFormat = null, $removeUsedContextFields = true); + + $message = $proc([ + 'message' => '{foo}', + 'context' => ['foo' => 'bar', 'lorem' => 'ipsum'], + ]); + $this->assertSame('bar', $message['message']); + $this->assertSame(['lorem' => 'ipsum'], $message['context']); } public function testCustomDateFormat() @@ -39,6 +52,7 @@ class PsrLogMessageProcessorTest extends \PHPUnit\Framework\TestCase 'context' => ['foo' => $date], ]); $this->assertEquals($date->format($format), $message['message']); + $this->assertSame(['foo' => $date], $message['context']); } public function getPairs() @@ -54,7 +68,11 @@ class PsrLogMessageProcessorTest extends \PHPUnit\Framework\TestCase [false, ''], [$date, $date->format(PsrLogMessageProcessor::SIMPLE_DATE)], [new \stdClass, '[object stdClass]'], - [[], '[array]'], + [[], 'array[]'], + [[], 'array[]'], + [[1, 2, 3], 'array[1,2,3]'], + [['foo' => 'bar'], 'array{"foo":"bar"}'], + [stream_context_create(), '[resource]'], ]; } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d475dd33..688777d3 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,3 +12,9 @@ date_default_timezone_set('UTC'); require __DIR__.'/../vendor/autoload.php'; + +// B.C. for PSR Log's old inheritance +// see https://github.com/php-fig/log/pull/52 +if (!class_exists('\\PHPUnit_Framework_TestCase', true)) { + class_alias('\\PHPUnit\\Framework\\TestCase', '\\PHPUnit_Framework_TestCase'); +}