2
0
Jordi Boggiano 3 жил өмнө
parent
commit
7684fae8fa

+ 1 - 1
.gitattributes

@@ -1,7 +1,7 @@
 /doc export-ignore
 /tests export-ignore
 /.* export-ignore
-/phpstan.neon.dist export-ignore
+/phpstan* export-ignore
 /phpunit.xml.dist export-ignore
 /_config.yml export-ignore
 /UPGRADE.md export-ignore

+ 1 - 1
.github/ISSUE_TEMPLATE/Bug_Report.md

@@ -4,6 +4,6 @@ about: Create a bug report
 labels: Bug
 ---
 
-Monolog version 1|2
+Monolog version 1|2|3?
 
 Write your bug report here.

+ 1 - 1
.github/ISSUE_TEMPLATE/Question.md

@@ -4,6 +4,6 @@ about: Ask a question regarding software usage
 labels: Support
 ---
 
-Monolog version 1|2
+Monolog version 1|2|3?
 
 Write your question here.

+ 20 - 5
.github/workflows/continuous-integration.yml

@@ -6,7 +6,7 @@ on:
 
 jobs:
   tests:
-    name: "CI"
+    name: "CI (PHP ${{ matrix.php-version }}, ${{ matrix.dependencies }} deps)"
 
     runs-on: "${{ matrix.operating-system }}"
 
@@ -19,6 +19,8 @@ jobs:
 
         dependencies: [highest]
 
+        composer-options: [""]
+
         operating-system:
           - "ubuntu-latest"
 
@@ -26,6 +28,10 @@ jobs:
           - php-version: "8.1"
             dependencies: lowest
             operating-system: ubuntu-latest
+          - php-version: "8.2"
+            dependencies: highest
+            operating-system: ubuntu-latest
+            composer-options: "--ignore-platform-req=php+"
 
     steps:
       - name: "Checkout"
@@ -61,9 +67,10 @@ jobs:
           composer config --no-plugins allow-plugins.ocramius/package-versions true
 
       - name: "Update dependencies with composer"
-        uses: "ramsey/composer-install@v1"
+        uses: "ramsey/composer-install@v2"
         with:
           dependency-versions: "${{ matrix.dependencies }}"
+          composer-options: "${{ matrix.composer-options }}"
 
       - name: "Run tests"
         run: "composer exec phpunit -- --exclude-group Elasticsearch,Elastica --verbose"
@@ -73,7 +80,7 @@ jobs:
         run: |
           composer remove --no-update --dev graylog2/gelf-php ruflin/elastica elasticsearch/elasticsearch rollbar/rollbar
           composer require --no-update psr/log:^3
-          composer update -W
+          composer update ${{ matrix.composer-options }}
           composer exec phpunit -- --exclude-group Elasticsearch,Elastica --verbose
 
   tests-es-7:
@@ -131,8 +138,12 @@ jobs:
       - name: "Change dependencies"
         run: "composer require --no-update --no-interaction --dev elasticsearch/elasticsearch:^${{ matrix.es-version }}"
 
+      - name: "Allow composer plugin to run"
+        if: "matrix.php-version == '7.4' && matrix.dependencies == 'lowest'"
+        run: "composer config allow-plugins.ocramius/package-versions true"
+
       - name: "Update dependencies with composer"
-        uses: "ramsey/composer-install@v1"
+        uses: "ramsey/composer-install@v2"
         with:
           dependency-versions: "${{ matrix.dependencies }}"
 
@@ -205,8 +216,12 @@ jobs:
           composer remove --no-update --dev graylog2/gelf-php ruflin/elastica elasticsearch/elasticsearch rollbar/rollbar
           composer require --no-update --no-interaction --dev elasticsearch/elasticsearch:^8
 
+      - name: "Allow composer plugin to run"
+        if: "matrix.php-version == '7.4' && matrix.dependencies == 'lowest'"
+        run: "composer config allow-plugins.ocramius/package-versions true"
+
       - name: "Update dependencies with composer"
-        uses: "ramsey/composer-install@v1"
+        uses: "ramsey/composer-install@v2"
         with:
           dependency-versions: "${{ matrix.dependencies }}"
 

+ 0 - 2
composer.json

@@ -26,7 +26,6 @@
         "guzzlehttp/psr7": "^2.2",
         "mongodb/mongodb": "^1.8",
         "php-amqplib/php-amqplib": "~2.4 || ^3",
-        "php-console/php-console": "^3.1.3",
         "phpstan/phpstan": "^1.4",
         "phpstan/phpstan-deprecation-rules": "^1.0",
         "phpstan/phpstan-strict-rules": "^1.1",
@@ -47,7 +46,6 @@
         "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
         "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
         "rollbar/rollbar": "Allow sending log messages to Rollbar",
-        "php-console/php-console": "Allow sending log messages to Google Chrome",
         "ext-mbstring": "Allow to work properly with unicode symbols",
         "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
         "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",

+ 0 - 3
doc/02-handlers-formatters-processors.md

@@ -51,7 +51,6 @@
   [php-amqplib](https://github.com/php-amqplib/php-amqplib) library.
 - [_GelfHandler_](https://github.com/Seldaek/monolog/blob/main/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_](https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/CubeHandler.php): Logs records to a [Cube](http://square.github.com/cube/) server.
 - [_ZendMonitorHandler_](https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/ZendMonitorHandler.php): Logs records to the Zend Monitor present in Zend Server.
 - [_NewRelicHandler_](https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/NewRelicHandler.php): Logs records to a [NewRelic](http://newrelic.com/) application.
 - [_LogglyHandler_](https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/LogglyHandler.php): Logs records to a [Loggly](http://www.loggly.com/) account.
@@ -72,8 +71,6 @@
   inline `console` messages within Chrome.
 - [_BrowserConsoleHandler_](https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/BrowserConsoleHandler.php): Handler to send logs to browser's Javascript `console` with
   no browser extension required. Most browsers supporting `console` API are supported.
-- [_PHPConsoleHandler_](https://github.com/Seldaek/monolog/blob/main/src/Monolog/Handler/PHPConsoleHandler.php): Handler for [PHP Console](https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef), providing
-  inline `console` and notification popup messages within Chrome.
 
 ### Log to databases
 

+ 14 - 4
phpstan-baseline.neon

@@ -5,6 +5,11 @@ parameters:
 			count: 1
 			path: src/Monolog/ErrorHandler.php
 
+		-
+			message: "#^Return type \\(array\\<array\\|bool\\|float\\|int\\|object\\|string\\|null\\>\\|bool\\|float\\|int\\|object\\|string\\|null\\) of method Monolog\\\\Formatter\\\\JsonFormatter\\:\\:normalize\\(\\) should be covariant with return type \\(array\\<array\\|bool\\|float\\|int\\|string\\|null\\>\\|bool\\|float\\|int\\|string\\|null\\) of method Monolog\\\\Formatter\\\\NormalizerFormatter\\:\\:normalize\\(\\)$#"
+			count: 1
+			path: src/Monolog/Formatter/JsonFormatter.php
+
 		-
 			message: "#^Cannot access offset 'table' on array\\<array\\|bool\\|float\\|int\\|string\\|null\\>\\|bool\\|float\\|int\\|object\\|string\\.$#"
 			count: 1
@@ -71,14 +76,14 @@ parameters:
 			path: src/Monolog/Handler/MandrillHandler.php
 
 		-
-			message: "#^Method Monolog\\\\Handler\\\\PHPConsoleHandler\\:\\:initOptions\\(\\) should return array\\{enabled\\: bool, classesPartialsTraceIgnore\\: array\\<string\\>, debugTagsKeysInContext\\: array\\<int\\|string\\>, useOwnErrorsHandler\\: bool, useOwnExceptionsHandler\\: bool, sourcesBasePath\\: string\\|null, registerHelper\\: bool, serverEncoding\\: string\\|null, \\.\\.\\.\\} but returns non\\-empty\\-array\\<'classesPartialsTrac…'\\|'dataStorage'\\|'debugTagsKeysInCont…'\\|'detectDumpTraceAndS…'\\|'dumperDetectCallbac…'\\|'dumperDumpSizeLimit'\\|'dumperItemsCountLim…'\\|'dumperItemSizeLimit'\\|'dumperLevelLimit'\\|'enabled'\\|'enableEvalListener'\\|'enableSslOnlyMode'\\|'headersLimit'\\|'ipMasks'\\|'password'\\|'registerHelper'\\|'serverEncoding'\\|'sourcesBasePath'\\|'useOwnErrorsHandler'\\|'useOwnExceptionsHan…', array\\<int\\|string\\>\\|bool\\|int\\|PhpConsole\\\\Storage\\|string\\|null\\>\\.$#"
+			message: "#^Instanceof between Monolog\\\\Handler\\\\HandlerInterface and Monolog\\\\Handler\\\\HandlerInterface will always evaluate to true\\.$#"
 			count: 1
-			path: src/Monolog/Handler/PHPConsoleHandler.php
+			path: src/Monolog/Handler/SamplingHandler.php
 
 		-
-			message: "#^Instanceof between Monolog\\\\Handler\\\\HandlerInterface and Monolog\\\\Handler\\\\HandlerInterface will always evaluate to true\\.$#"
+			message: "#^Match expression does not handle remaining value\\: 'EMERGENCY'$#"
 			count: 1
-			path: src/Monolog/Handler/SamplingHandler.php
+			path: src/Monolog/Level.php
 
 		-
 			message: "#^Variable property access on \\$this\\(Monolog\\\\LogRecord\\)\\.$#"
@@ -110,3 +115,8 @@ parameters:
 			count: 1
 			path: src/Monolog/Processor/UidProcessor.php
 
+		-
+			message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\<int\\|string, string\\>\\)\\: string, Closure\\(mixed\\)\\: array\\<int, string\\>\\|string\\|false given\\.$#"
+			count: 1
+			path: src/Monolog/Utils.php
+

+ 3 - 0
phpstan.neon.dist

@@ -8,6 +8,9 @@ parameters:
         - src/
 #        - tests/
 
+    excludePaths:
+        - 'src/Monolog/Handler/PHPConsoleHandler.php'
+
     ignoreErrors:
         - '#zend_monitor_|ZEND_MONITOR_#'
         - '#MongoDB\\(Client|Collection)#'

+ 21 - 5
src/Monolog/Formatter/JsonFormatter.php

@@ -11,6 +11,7 @@
 
 namespace Monolog\Formatter;
 
+use Stringable;
 use Throwable;
 use Monolog\LogRecord;
 
@@ -139,6 +140,8 @@ class JsonFormatter extends NormalizerFormatter
 
     /**
      * Normalizes given $data.
+     *
+     * @return null|scalar|array<mixed[]|scalar|null|object>|object
      */
     protected function normalize(mixed $data, int $depth = 0): mixed
     {
@@ -162,12 +165,25 @@ class JsonFormatter extends NormalizerFormatter
             return $normalized;
         }
 
-        if ($data instanceof \DateTimeInterface) {
-            return $this->formatDate($data);
-        }
+        if (is_object($data)) {
+            if ($data instanceof \DateTimeInterface) {
+                return $this->formatDate($data);
+            }
+
+            if ($data instanceof Throwable) {
+                return $this->normalizeException($data, $depth);
+            }
+
+            // if the object has specific json serializability we want to make sure we skip the __toString treatment below
+            if ($data instanceof \JsonSerializable) {
+                return $data;
+            }
+
+            if ($data instanceof Stringable) {
+                return $data->__toString();
+            }
 
-        if ($data instanceof Throwable) {
-            return $this->normalizeException($data, $depth);
+            return $data;
         }
 
         if (is_resource($data)) {

+ 6 - 0
src/Monolog/Formatter/LineFormatter.php

@@ -149,6 +149,12 @@ class LineFormatter extends NormalizerFormatter
 
         if (($previous = $e->getPrevious()) instanceof \Throwable) {
             do {
+                $depth++;
+                if ($depth > $this->maxNormalizeDepth) {
+                    $str .= '\n[previous exception] Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
+                    break;
+                }
+
                 $str .= "\n[previous exception] " . $this->formatException($previous);
             } while ($previous = $previous->getPrevious());
         }

+ 4 - 0
src/Monolog/Formatter/NormalizerFormatter.php

@@ -218,6 +218,10 @@ class NormalizerFormatter implements FormatterInterface
      */
     protected function normalizeException(Throwable $e, int $depth = 0)
     {
+        if ($depth > $this->maxNormalizeDepth) {
+            return ['Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization'];
+        }
+
         if ($e instanceof \JsonSerializable) {
             return (array) $e->jsonSerialize();
         }

+ 34 - 4
src/Monolog/Handler/AmqpHandler.php

@@ -23,6 +23,9 @@ class AmqpHandler extends AbstractProcessingHandler
 {
     protected AMQPExchange|AMQPChannel $exchange;
 
+    /** @var array<string, mixed> */
+    private array $extraAttributes = [];
+
     protected string $exchangeName;
 
     /**
@@ -41,6 +44,29 @@ class AmqpHandler extends AbstractProcessingHandler
         parent::__construct($level, $bubble);
     }
 
+    /**
+     * @return array<string, mixed>
+     */
+    public function getExtraAttributes(): array
+    {
+        return $this->extraAttributes;
+    }
+
+    /**
+     * Configure extra attributes to pass to the AMQPExchange (if you are using the amqp extension)
+     *
+     * @param array<string, mixed> $extraAttributes  One of content_type, content_encoding,
+     *                                               message_id, user_id, app_id, delivery_mode,
+     *                                               priority, timestamp, expiration, type
+     *                                               or reply_to, headers.
+     * @return $this
+     */
+    public function setExtraAttributes(array $extraAttributes): self
+    {
+        $this->extraAttributes = $extraAttributes;
+        return $this;
+    }
+
     /**
      * @inheritDoc
      */
@@ -50,14 +76,18 @@ class AmqpHandler extends AbstractProcessingHandler
         $routingKey = $this->getRoutingKey($record);
 
         if ($this->exchange instanceof AMQPExchange) {
+            $attributes = [
+                'delivery_mode' => 2,
+                'content_type'  => 'application/json',
+            ];
+            if (\count($this->extraAttributes) > 0) {
+                $attributes = array_merge($attributes, $this->extraAttributes);
+            }
             $this->exchange->publish(
                 $data,
                 $routingKey,
                 0,
-                [
-                    'delivery_mode' => 2,
-                    'content_type' => 'application/json',
-                ]
+                $attributes
             );
         } else {
             $this->exchange->basic_publish(

+ 3 - 3
src/Monolog/Handler/ChromePHPHandler.php

@@ -145,7 +145,7 @@ class ChromePHPHandler extends AbstractProcessingHandler
         }
 
         $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
-        $data = base64_encode(utf8_encode($json));
+        $data = base64_encode($json);
         if (strlen($data) > 3 * 1024) {
             self::$overflowed = true;
 
@@ -156,8 +156,8 @@ class ChromePHPHandler extends AbstractProcessingHandler
                 datetime: new DateTimeImmutable(true),
             );
             self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
-            $json = Utils::jsonEncode(self::$json, null, true);
-            $data = base64_encode(utf8_encode($json));
+            $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
+            $data = base64_encode($json);
         }
 
         if (trim($data) !== '') {

+ 2 - 1
src/Monolog/Handler/CubeHandler.php

@@ -18,8 +18,9 @@ use Monolog\LogRecord;
 /**
  * Logs to Cube.
  *
- * @link http://square.github.com/cube/
+ * @link https://github.com/square/cube/wiki
  * @author Wan Chen <kami@kamisama.me>
+ * @deprecated Since 2.8.0 and 3.2.0, Cube appears abandoned and thus we will drop this handler in Monolog 4
  */
 class CubeHandler extends AbstractProcessingHandler
 {

+ 3 - 1
src/Monolog/Handler/PHPConsoleHandler.php

@@ -27,7 +27,7 @@ use PhpConsole\Storage;
  * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely
  *
  * Usage:
- * 1. Install Google Chrome extension https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef
+ * 1. Install Google Chrome extension [now dead and removed from the chrome store]
  * 2. See overview https://github.com/barbushin/php-console#overview
  * 3. Install PHP Console library https://github.com/barbushin/php-console#installation
  * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png)
@@ -83,6 +83,8 @@ use PhpConsole\Storage;
  *     detectDumpTraceAndSource?: bool,
  *     dataStorage?: Storage|null
  * }
+ *
+ * @deprecated Since 2.8.0 and 3.2.0, PHPConsole is abandoned and thus we will drop this handler in Monolog 4
  */
 class PHPConsoleHandler extends AbstractProcessingHandler
 {

+ 1 - 1
src/Monolog/Handler/RedisPubSubHandler.php

@@ -32,7 +32,7 @@ use Redis;
 class RedisPubSubHandler extends AbstractProcessingHandler
 {
     /** @var Predis<Predis>|Redis */
-    private $redisClient;
+    private Predis|Redis $redisClient;
     private string $channelKey;
 
     /**

+ 1 - 1
src/Monolog/Handler/StreamHandler.php

@@ -193,7 +193,7 @@ class StreamHandler extends AbstractProcessingHandler
             set_error_handler([$this, 'customErrorHandler']);
             $status = mkdir($dir, 0777, true);
             restore_error_handler();
-            if (false === $status && !is_dir($dir)) {
+            if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) {
                 throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir));
             }
         }

+ 1 - 1
src/Monolog/Utils.php

@@ -200,7 +200,7 @@ final class Utils
             $data = preg_replace_callback(
                 '/[\x80-\xFF]+/',
                 function ($m) {
-                    return utf8_encode($m[0]);
+                    return function_exists('mb_convert_encoding') ? mb_convert_encoding($m[0], 'UTF-8', 'ISO-8859-1') : utf8_encode($m[0]);
                 },
                 $data
             );

+ 55 - 0
tests/Monolog/Formatter/JsonFormatterTest.php

@@ -13,6 +13,7 @@ namespace Monolog\Formatter;
 
 use Monolog\Level;
 use Monolog\LogRecord;
+use JsonSerializable;
 use Monolog\Test\TestCase;
 
 class JsonFormatterTest extends TestCase
@@ -289,4 +290,58 @@ class JsonFormatterTest extends TestCase
             $record
         );
     }
+
+    public function testFormatObjects()
+    {
+        $formatter = new JsonFormatter();
+
+        $record = $formatter->format($this->getRecord(
+            Level::Debug,
+            'Testing',
+            channel: 'test',
+            datetime: new \DateTimeImmutable('2022-02-22 00:00:00'),
+            context: [
+                'public' => new TestJsonNormPublic,
+                'private' => new TestJsonNormPrivate,
+                'withToStringAndJson' => new TestJsonNormWithToStringAndJson,
+                'withToString' => new TestJsonNormWithToString,
+            ],
+        ));
+
+        $this->assertSame(
+            '{"message":"Testing","context":{"public":{"foo":"fooValue"},"private":{},"withToStringAndJson":["json serialized"],"withToString":"stringified"},"level":100,"level_name":"DEBUG","channel":"test","datetime":"2022-02-22T00:00:00+00:00","extra":{}}'."\n",
+            $record
+        );
+    }
+}
+
+class TestJsonNormPublic
+{
+    public $foo = 'fooValue';
+}
+
+class TestJsonNormPrivate
+{
+    private $foo = 'fooValue';
+}
+
+class TestJsonNormWithToStringAndJson implements JsonSerializable
+{
+    public function jsonSerialize()
+    {
+        return ['json serialized'];
+    }
+
+    public function __toString()
+    {
+        return 'SHOULD NOT SHOW UP';
+    }
+}
+
+class TestJsonNormWithToString
+{
+    public function __toString()
+    {
+        return 'stringified';
+    }
 }

+ 3 - 1
tests/Monolog/Formatter/NormalizerFormatterTest.php

@@ -364,8 +364,10 @@ class NormalizerFormatterTest extends TestCase
         $record = $this->getRecord(context: ['exception' => $e]);
         $result = $formatter->format($record);
 
+        // See https://github.com/php/php-src/issues/8810 fixed in PHP 8.2
+        $offset = PHP_VERSION_ID >= 80200 ? 13 : 11;
         $this->assertSame(
-            __FILE__.':'.(__LINE__-9),
+            __FILE__.':'.(__LINE__ - $offset),
             $result['context']['exception']['trace'][0]
         );
     }

+ 6 - 6
tests/Monolog/Handler/ChromePHPHandlerTest.php

@@ -38,7 +38,7 @@ class ChromePHPHandlerTest extends TestCase
         $handler->handle($this->getRecord(Level::Warning));
 
         $expected = [
-            'X-ChromeLogger-Data'   => base64_encode(utf8_encode(json_encode([
+            'X-ChromeLogger-Data'   => base64_encode(json_encode([
                 'version' => '4.0',
                 'columns' => ['label', 'log', 'backtrace', 'type'],
                 'rows' => [
@@ -46,7 +46,7 @@ class ChromePHPHandlerTest extends TestCase
                     'test',
                 ],
                 'request_uri' => '',
-            ]))),
+            ])),
         ];
 
         $this->assertEquals($expected, $handler->getHeaders());
@@ -72,7 +72,7 @@ class ChromePHPHandlerTest extends TestCase
         $handler->handle($this->getRecord(Level::Warning, str_repeat('b', 2 * 1024)));
 
         $expected = [
-            'X-ChromeLogger-Data'   => base64_encode(utf8_encode(json_encode([
+            'X-ChromeLogger-Data'   => base64_encode(json_encode([
                 'version' => '4.0',
                 'columns' => ['label', 'log', 'backtrace', 'type'],
                 'rows' => [
@@ -96,7 +96,7 @@ class ChromePHPHandlerTest extends TestCase
                     ],
                 ],
                 'request_uri' => '',
-            ]))),
+            ])),
         ];
 
         $this->assertEquals($expected, $handler->getHeaders());
@@ -115,7 +115,7 @@ class ChromePHPHandlerTest extends TestCase
         $handler2->handle($this->getRecord(Level::Warning));
 
         $expected = [
-            'X-ChromeLogger-Data'   => base64_encode(utf8_encode(json_encode([
+            'X-ChromeLogger-Data'   => base64_encode(json_encode([
                 'version' => '4.0',
                 'columns' => ['label', 'log', 'backtrace', 'type'],
                 'rows' => [
@@ -125,7 +125,7 @@ class ChromePHPHandlerTest extends TestCase
                     'test',
                 ],
                 'request_uri' => '',
-            ]))),
+            ])),
         ];
 
         $this->assertEquals($expected, $handler2->getHeaders());