Repository: Seldaek/monolog (php)

Issue:
Formatter Syntax (Documentation Not Available)
There's no obvious cheat sheet for what substitutions one can make for the Formatters (I'm specifically thinking of the "LineFormatter". I've tried to follow the code backwards via inheritance and can see that it extends the NormalizeFormatter but its not clear to me how or where the substitutions are made.

If it were, I'd have proposed a Pull Request for such documentation.

1. It's probably somewhere really obvious - but if it is can someone point me to it (and possibly make it more obvious)?
2. If it's really obvious, as in the code where the substitutions are made is obvious, could someone point me to the code so I can read/understand and make a Pull Request to help out?

If it's not really obvious, how might we better document what one can put into the format string for the LineFormatter and possibly other relevant loggers?

I'm using the latest version of Monolog from composer (composer update monolog/monolog) and looking in the latest MASTER on this GitHub.

(I'd add this to the documentation label but I'm not sure how to do that either :()

Pull request:
Documentation. Enrich documentation on "Customizing log format".
Fixes #889 

The idea behind this pull request is to put a little bit more light and understanding on the "Customizing log format" section of the help.

Not only mentioned in the issue referenced here but also happened to some colleges too.

Hopefully is good enough.

Ideas and suggestions are welcome too.

Patch:
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 9852d780..5cb0839c 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -51,18 +51,9 @@ jobs:
           key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
           restore-keys: ${{ runner.os }}-composer-
 
-      - name: Remove elasticsearch/elasticsearch on PHP 8
-        if: "startsWith(matrix.php-version, '8.')"
-        run: 'composer remove elasticsearch/elasticsearch --dev --no-update'
-
       - name: Add require for mongodb/mongodb to make tests runnable
         run: 'composer require ${{ env.COMPOSER_FLAGS }} mongodb/mongodb --dev --no-update'
 
-      # This does not affect runtime, only tests were fixed in psr/log 1.1.2 so it's
-      # ok to require this only when running tests
-      - name: Bump required version of psr/log for tests purposes to fix the --prefer-lowest builds
-        run: 'composer require ${{ env.COMPOSER_FLAGS }} psr/log:^1.1.2 --no-update'
-
       - name: "Handle lowest dependencies update"
         if: "contains(matrix.dependencies, 'lowest')"
         run: "echo \"COMPOSER_FLAGS=$COMPOSER_FLAGS --prefer-lowest\" >> $GITHUB_ENV"
@@ -73,3 +64,11 @@ jobs:
 
       - name: "Run tests"
         run: "composer exec phpunit -- --verbose"
+
+      - name: "Run tests with psr/log 3"
+        if: "contains(matrix.dependencies, 'highest') && matrix.php-version >= '8.0'"
+        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 ${{ env.COMPOSER_FLAGS }}
+          composer exec phpunit -- --verbose
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3f46977..121380fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+### 2.3.5 (2021-10-01)
+
+  * Fixed regression in StreamHandler since 2.3.3 on systems with the memory_limit set to >=20GB (#1592)
+
+### 2.3.4 (2021-09-15)
+
+  * Fixed support for psr/log 3.x (#1589)
+
+### 2.3.3 (2021-09-14)
+
+  * Fixed memory usage when using StreamHandler and calling stream_get_contents on the resource you passed to it (#1578, #1577)
+  * Fixed support for psr/log 2.x (#1587)
+  * Fixed some type annotations
+
 ### 2.3.2 (2021-07-23)
 
   * Fixed compatibility with PHP 7.2 - 7.4 when experiencing PCRE errors (#1568)
diff --git a/README.md b/README.md
index 0a46feeb..e7104af7 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/mono
 - [Slim](http://www.slimframework.com/) is usable with Monolog via the [Slim-Monolog](https://github.com/Flynsarmy/Slim-Monolog) log writer.
 - [XOOPS 2.6](http://xoops.org/) comes out of the box with Monolog.
 - [Aura.Web_Project](https://github.com/auraphp/Aura.Web_Project) comes out of the box with Monolog.
-- [Nette Framework](http://nette.org/en/) can be used with Monolog via [contributte/monolog](https://github.com/contributte/monolog) extension.
+- [Nette Framework](http://nette.org/en/) is usable with Monolog via the [contributte/monolog](https://github.com/contributte/monolog) or [orisai/nette-monolog](https://github.com/orisai/nette-monolog) extensions.
 - [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.
diff --git a/composer.json b/composer.json
index 7ed2e3d3..020d278e 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,7 @@
     ],
     "require": {
         "php": ">=7.2",
-        "psr/log": "^1.0.1"
+        "psr/log": "^1.0.1 || ^2.0 || ^3.0"
     },
     "require-dev": {
         "aws/aws-sdk-php": "^2.4.9 || ^3.0",
@@ -22,13 +22,13 @@
         "elasticsearch/elasticsearch": "^7",
         "mongodb/mongodb": "^1.8",
         "graylog2/gelf-php": "^1.4.2",
-        "php-amqplib/php-amqplib": "~2.4",
+        "php-amqplib/php-amqplib": "~2.4 || ^3",
         "php-console/php-console": "^3.1.3",
         "phpspec/prophecy": "^1.6.1",
         "phpunit/phpunit": "^8.5",
         "predis/predis": "^1.1",
         "rollbar/rollbar": "^1.3",
-        "ruflin/elastica": ">=0.90 <7.0.1",
+        "ruflin/elastica": ">=0.90@dev",
         "swiftmailer/swiftmailer": "^5.3|^6.0",
         "phpstan/phpstan": "^0.12.91"
     },
@@ -44,7 +44,10 @@
         "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-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",
+        "ext-openssl": "Required to send log messages using SSL"
     },
     "autoload": {
         "psr-4": {"Monolog\\": "src/Monolog"}
@@ -53,7 +56,7 @@
         "psr-4": {"Monolog\\": "tests/Monolog"}
     },
     "provide": {
-        "psr/log-implementation": "1.0.0"
+        "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0"
     },
     "extra": {
         "branch-alias": {
@@ -65,8 +68,8 @@
         "phpstan": "vendor/bin/phpstan analyse"
     },
     "config": {
+        "lock": false,
         "sort-packages": true,
         "platform-check": false
-    },
-    "lock": false
+    }
 }
diff --git a/doc/01-usage.md b/doc/01-usage.md
index fa37614e..2eae0418 100644
--- a/doc/01-usage.md
+++ b/doc/01-usage.md
@@ -109,10 +109,6 @@ Note that the FirePHPHandler is called first as it is added on top of the
 stack. This allows you to temporarily add a logger with bubbling disabled if
 you want to override other configured loggers.
 
-> If you use Monolog standalone and are looking for an easy way to
-> configure many handlers, the [theorchard/monolog-cascade](https://github.com/theorchard/monolog-cascade)
-> can help you build complex logging configs via PHP arrays, yaml or json configs.
-
 ## Adding extra data in the records
 
 Monolog provides two different ways to add extra information along the simple
@@ -194,31 +190,48 @@ $securityLogger = $logger->withName('security');
 ## Customizing the log format
 
 In Monolog it's easy to customize the format of the logs written into files,
-sockets, mails, databases and other handlers. Most of the handlers use the
+sockets, mails, databases and other handlers; by the use of "Formatters". 
 
+As mentioned before, a *Formatter* is attached to a *Handler*, and as a general convention, most of the handlers use the
 ```php
 $record['formatted']
 ```
+field in the log record to store its formatted value. Again, this field depends on the implemenation of the *Handler* but is a good idea to **stick into the good practices and conventions of the project**.
+
+You can choose between predefined formatter classes or write your own (e.g. a multiline text file for human-readable output).
 
-value to be automatically put into the log device. This value depends on the
-formatter settings. You can choose between predefined formatter classes or
-write your own (e.g. a multiline text file for human-readable output).
+> Note:
+> 
+> A very useful formatter to look at, is the `LineFormatter`.
+> 
+> This formatter, as its name might indicate, is able to return a lineal string representation of the log record provided.
+> 
+> It is also capable to interpolate values from the log record, into the output format template used by the formatter to generate the final result, and in order to do it, you need to provide the log record values you are interested in, in the output template string using the form %value%, e.g: "'%context.foo% => %extra.foo%'" , in this example the values $record["context"]["foo"] and $record["extra"]["foo"] will be render as part of th final result. 
 
-To configure a predefined formatter class, just set it as the handler's field:
+In the following example, we demonstrate how to:
+1. Create a `LineFormatter` instance and set a custom output format template.
+2. Create a new *Handler*.
+3. Attach the *Formatter* to the *Handler*.
+4. Create a new *Logger* object.
+5. Attach the *Handler* to the *Logger* object.
 
 ```php
 <?php
 
 // the default date format is "Y-m-d\TH:i:sP"
 $dateFormat = "Y n j, g:i a";
+
 // the default output format is "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"
+// we now change the default output format according our needs.
 $output = "%datetime% > %level_name% > %message% %context% %extra%\n";
+
 // finally, create a formatter
 $formatter = new LineFormatter($output, $dateFormat);
 
 // Create a handler
 $stream = new StreamHandler(__DIR__.'/my_app.log', Logger::DEBUG);
 $stream->setFormatter($formatter);
+
 // bind it to a logger object
 $securityLogger = new Logger('security');
 $securityLogger->pushHandler($stream);
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 961f7ce4..275089a0 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -35,3 +35,6 @@ parameters:
         - '#::popProcessor\(\) should return callable#'
         - '#Parameter \#1 \$ of callable \(callable\(Monolog\\Handler\\Record\): Monolog\\Handler\\Record\)#'
         - '#is incompatible with native type array.#'
+
+        # can be removed when https://github.com/rollbar/rollbar-php/pull/536 will be merged
+        - '#Rollbar\\RollbarLogger#'
diff --git a/src/Monolog/Handler/FilterHandler.php b/src/Monolog/Handler/FilterHandler.php
index 81d227b4..718f17ef 100644
--- a/src/Monolog/Handler/FilterHandler.php
+++ b/src/Monolog/Handler/FilterHandler.php
@@ -44,7 +44,7 @@ class FilterHandler extends Handler implements ProcessableHandlerInterface, Rese
      * Minimum level for logs that are passed to handler
      *
      * @var int[]
-     * @phpstan-var Level[]
+     * @phpstan-var array<Level, int>
      */
     protected $acceptedLevels;
 
@@ -78,7 +78,7 @@ class FilterHandler extends Handler implements ProcessableHandlerInterface, Rese
     }
 
     /**
-     * @phpstan-return Level[]
+     * @phpstan-return array<int, Level>
      */
     public function getAcceptedLevels(): array
     {
diff --git a/src/Monolog/Handler/IFTTTHandler.php b/src/Monolog/Handler/IFTTTHandler.php
index f43ef267..000ccea4 100644
--- a/src/Monolog/Handler/IFTTTHandler.php
+++ b/src/Monolog/Handler/IFTTTHandler.php
@@ -38,6 +38,10 @@ class IFTTTHandler extends AbstractProcessingHandler
      */
     public function __construct(string $eventName, string $secretKey, $level = Logger::ERROR, bool $bubble = true)
     {
+        if (!extension_loaded('curl')) {
+            throw new MissingExtensionException('The curl extension is needed to use the IFTTTHandler');
+        }
+
         $this->eventName = $eventName;
         $this->secretKey = $secretKey;
 
diff --git a/src/Monolog/Handler/SendGridHandler.php b/src/Monolog/Handler/SendGridHandler.php
index 4ec64290..1280ee70 100644
--- a/src/Monolog/Handler/SendGridHandler.php
+++ b/src/Monolog/Handler/SendGridHandler.php
@@ -59,6 +59,10 @@ class SendGridHandler extends MailHandler
      */
     public function __construct(string $apiUser, string $apiKey, string $from, $to, string $subject, $level = Logger::ERROR, bool $bubble = true)
     {
+        if (!extension_loaded('curl')) {
+            throw new MissingExtensionException('The curl extension is needed to use the SendGridHandler');
+        }
+
         parent::__construct($level, $bubble);
         $this->apiUser = $apiUser;
         $this->apiKey = $apiKey;
diff --git a/src/Monolog/Handler/SlackWebhookHandler.php b/src/Monolog/Handler/SlackWebhookHandler.php
index d6a3733e..8ae3c788 100644
--- a/src/Monolog/Handler/SlackWebhookHandler.php
+++ b/src/Monolog/Handler/SlackWebhookHandler.php
@@ -58,6 +58,10 @@ class SlackWebhookHandler extends AbstractProcessingHandler
         bool $bubble = true,
         array $excludeFields = array()
     ) {
+        if (!extension_loaded('curl')) {
+            throw new MissingExtensionException('The curl extension is needed to use the SlackWebhookHandler');
+        }
+
         parent::__construct($level, $bubble);
 
         $this->webhookUrl = $webhookUrl;
diff --git a/src/Monolog/Handler/SocketHandler.php b/src/Monolog/Handler/SocketHandler.php
index c98249e8..4e1410ab 100644
--- a/src/Monolog/Handler/SocketHandler.php
+++ b/src/Monolog/Handler/SocketHandler.php
@@ -36,7 +36,7 @@ class SocketHandler extends AbstractProcessingHandler
     private $writingTimeout = 10.0;
     /** @var ?int */
     private $lastSentBytes = null;
-    /** @var int */
+    /** @var ?int */
     private $chunkSize = null;
     /** @var bool */
     private $persistent = false;
@@ -197,7 +197,7 @@ class SocketHandler extends AbstractProcessingHandler
     /**
      * Get current chunk size
      */
-    public function getChunkSize(): int
+    public function getChunkSize(): ?int
     {
         return $this->chunkSize;
     }
@@ -265,6 +265,10 @@ class SocketHandler extends AbstractProcessingHandler
             throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource');
         }
 
+        if (null === $this->chunkSize) {
+            throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set');
+        }
+
         return stream_set_chunk_size($this->resource, $this->chunkSize);
     }
 
diff --git a/src/Monolog/Handler/StreamHandler.php b/src/Monolog/Handler/StreamHandler.php
index bc1f198d..2531d41b 100644
--- a/src/Monolog/Handler/StreamHandler.php
+++ b/src/Monolog/Handler/StreamHandler.php
@@ -25,8 +25,12 @@ use Monolog\Utils;
  */
 class StreamHandler extends AbstractProcessingHandler
 {
+    /** @const int */
     protected const MAX_CHUNK_SIZE = 2147483647;
-
+    /** @const int 10MB */
+    protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
+    /** @var int */
+    protected $streamChunkSize;
     /** @var resource|null */
     protected $stream;
     /** @var ?string */
@@ -50,9 +54,24 @@ class StreamHandler extends AbstractProcessingHandler
     public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false)
     {
         parent::__construct($level, $bubble);
+
+        if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
+            if ($phpMemoryLimit > 0) {
+                // use max 10% of allowed memory for the chunk size, and at least 100KB
+                $this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024));
+            } else {
+                // memory is unlimited, set to the default 10MB
+                $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
+            }
+        } else {
+            // no memory limit information, set to the default 10MB
+            $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
+        }
+
         if (is_resource($stream)) {
             $this->stream = $stream;
-            stream_set_chunk_size($this->stream, self::MAX_CHUNK_SIZE);
+
+            stream_set_chunk_size($this->stream, $this->streamChunkSize);
         } elseif (is_string($stream)) {
             $this->url = Utils::canonicalizePath($stream);
         } else {
@@ -95,6 +114,14 @@ class StreamHandler extends AbstractProcessingHandler
         return $this->url;
     }
 
+    /**
+     * @return int
+     */
+    public function getStreamChunkSize(): int
+    {
+        return $this->streamChunkSize;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -118,7 +145,7 @@ class StreamHandler extends AbstractProcessingHandler
 
                 throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url));
             }
-            stream_set_chunk_size($stream, self::MAX_CHUNK_SIZE);
+            stream_set_chunk_size($stream, $this->streamChunkSize);
             $this->stream = $stream;
         }
 
diff --git a/src/Monolog/Handler/SyslogUdpHandler.php b/src/Monolog/Handler/SyslogUdpHandler.php
index abc6d44d..deaa19f8 100644
--- a/src/Monolog/Handler/SyslogUdpHandler.php
+++ b/src/Monolog/Handler/SyslogUdpHandler.php
@@ -49,11 +49,16 @@ class SyslogUdpHandler extends AbstractSyslogHandler
      * @param bool       $bubble   Whether the messages that are handled can bubble up the stack or not
      * @param string     $ident    Program name or tag for each log message.
      * @param int        $rfc      RFC to format the message for.
+     * @throws MissingExtensionException
      *
      * @phpstan-param self::RFC* $rfc
      */
     public function __construct(string $host, int $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, string $ident = 'php', int $rfc = self::RFC5424)
     {
+        if (!extension_loaded('sockets')) {
+            throw new MissingExtensionException('The sockets extension is required to use the SyslogUdpHandler');
+        }
+
         parent::__construct($facility, $level, $bubble);
 
         $this->ident = $ident;
diff --git a/src/Monolog/Handler/TelegramBotHandler.php b/src/Monolog/Handler/TelegramBotHandler.php
index 013cfdd8..9ea05737 100644
--- a/src/Monolog/Handler/TelegramBotHandler.php
+++ b/src/Monolog/Handler/TelegramBotHandler.php
@@ -90,6 +90,10 @@ class TelegramBotHandler extends AbstractProcessingHandler
         bool $disableWebPagePreview = null,
         bool $disableNotification = null
     ) {
+        if (!extension_loaded('curl')) {
+            throw new MissingExtensionException('The curl extension is needed to use the TelegramBotHandler');
+        }
+
         parent::__construct($level, $bubble);
 
         $this->apiKey = $apiKey;
diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php
index 37086e7e..9fbf2052 100644
--- a/src/Monolog/Logger.php
+++ b/src/Monolog/Logger.php
@@ -17,6 +17,7 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\InvalidArgumentException;
 use Psr\Log\LogLevel;
 use Throwable;
+use Stringable;
 
 /**
  * Monolog log channel
@@ -382,6 +383,7 @@ class Logger implements LoggerInterface, ResettableInterface
      * Gets all supported logging levels.
      *
      * @return array<string, int> Assoc array with human-readable level names => level codes.
+     * @phpstan-return array<LevelName, Level>
      */
     public static function getLevels(): array
     {
@@ -481,14 +483,18 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param int|string $level   The log level
-     * @param string     $message The log message
-     * @param mixed[]    $context The log context
+     * @param mixed             $level   The log level
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      *
      * @phpstan-param Level|LevelName|LogLevel::* $level
      */
     public function log($level, $message, array $context = []): void
     {
+        if (!is_int($level) && !is_string($level)) {
+            throw new \InvalidArgumentException('$level is expected to be a string or int');
+        }
+
         $level = static::toMonologLevel($level);
 
         $this->addRecord($level, (string) $message, $context);
@@ -499,8 +505,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function debug($message, array $context = []): void
     {
@@ -512,8 +518,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function info($message, array $context = []): void
     {
@@ -525,8 +531,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function notice($message, array $context = []): void
     {
@@ -538,8 +544,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function warning($message, array $context = []): void
     {
@@ -551,8 +557,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function error($message, array $context = []): void
     {
@@ -564,8 +570,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function critical($message, array $context = []): void
     {
@@ -577,8 +583,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function alert($message, array $context = []): void
     {
@@ -590,8 +596,8 @@ class Logger implements LoggerInterface, ResettableInterface
      *
      * This method allows for compatibility with common interfaces.
      *
-     * @param string  $message The log message
-     * @param mixed[] $context The log context
+     * @param string|Stringable $message The log message
+     * @param mixed[]           $context The log context
      */
     public function emergency($message, array $context = []): void
     {
diff --git a/src/Monolog/Utils.php b/src/Monolog/Utils.php
index 8812f007..d3e7ad09 100644
--- a/src/Monolog/Utils.php
+++ b/src/Monolog/Utils.php
@@ -226,4 +226,38 @@ final class Utils
             );
         }
     }
+
+    /**
+     * Converts a string with a valid 'memory_limit' format, to bytes.
+     *
+     * @param string|false $val
+     * @return int|false Returns an integer representing bytes. Returns FALSE in case of error.
+     */
+    public static function expandIniShorthandBytes($val)
+    {
+        if (!is_string($val)) {
+            return false;
+        }
+
+        // support -1
+        if ((int) $val < 0) {
+            return (int) $val;
+        }
+
+        if (!preg_match('/^\s*(?<val>\d+)(?:\.\d+)?\s*(?<unit>[gmk]?)\s*$/i', $val, $match)) {
+            return false;
+        }
+
+        $val = (int) $match['val'];
+        switch (strtolower($match['unit'] ?? '')) {
+            case 'g':
+                $val *= 1024;
+            case 'm':
+                $val *= 1024;
+            case 'k':
+                $val *= 1024;
+        }
+
+        return $val;
+    }
 }


Test patch:
diff --git a/tests/Monolog/Handler/RollbarHandlerTest.php b/tests/Monolog/Handler/RollbarHandlerTest.php
index 40442105..0aa7cbff 100644
--- a/tests/Monolog/Handler/RollbarHandlerTest.php
+++ b/tests/Monolog/Handler/RollbarHandlerTest.php
@@ -22,6 +22,8 @@ use Rollbar\RollbarLogger;
  * @see    https://rollbar.com/docs/notifier/rollbar-php/
  *
  * @coversDefaultClass Monolog\Handler\RollbarHandler
+ *
+ * @requires function \Rollbar\RollbarLogger::__construct
  */
 class RollbarHandlerTest extends TestCase
 {
diff --git a/tests/Monolog/Handler/StreamHandlerTest.php b/tests/Monolog/Handler/StreamHandlerTest.php
index 8e0d111f..275aa955 100644
--- a/tests/Monolog/Handler/StreamHandlerTest.php
+++ b/tests/Monolog/Handler/StreamHandlerTest.php
@@ -11,6 +11,7 @@
 
 namespace Monolog\Handler;
 
+use Monolog\Handler\StreamHandler;
 use Monolog\Test\TestCase;
 use Monolog\Logger;
 
@@ -220,4 +221,65 @@ class StreamHandlerTest extends TestCase
             ],
         ];
     }
+
+    public function provideMemoryValues()
+    {
+        return [
+            ['1M', (int) (1024*1024/10)],
+            ['10M', (int) (1024*1024)],
+            ['1024M', (int) (1024*1024*1024/10)],
+            ['1G', (int) (1024*1024*1024/10)],
+            ['2000M', (int) (2000*1024*1024/10)],
+            ['2050M', (int) (2050*1024*1024/10)],
+            ['2048M', (int) (2048*1024*1024/10)],
+            ['3G', (int) (3*1024*1024*1024/10)],
+            ['2560M', (int) (2560*1024*1024/10)],
+        ];
+    }
+
+    /**
+     * @dataProvider provideMemoryValues
+     * @return void
+     */
+    public function testPreventOOMError($phpMemory, $expectedChunkSize)
+    {
+        $previousValue = ini_set('memory_limit', $phpMemory);
+
+        if ($previousValue === false) {
+            $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
+        }
+
+        $stream = tmpfile();
+
+        if ($stream === false) {
+            $this->markTestSkipped('We could not create a temp file to be use as a stream.');
+        }
+
+        $exceptionRaised = false;
+
+        $handler = new StreamHandler($stream);
+        stream_get_contents($stream, 1024);
+
+        ini_set('memory_limit', $previousValue);
+
+        $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
+    }
+
+    /**
+     * @return void
+     */
+    public function testSimpleOOMPrevention()
+    {
+        $previousValue = ini_set('memory_limit', '2048M');
+
+        if ($previousValue === false) {
+            $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
+        }
+
+        $stream = tmpfile();
+        new StreamHandler($stream);
+        stream_get_contents($stream);
+        ini_set('memory_limit', $previousValue);
+        $this->assertTrue(true);
+    }
 }
diff --git a/tests/Monolog/PsrLogCompatTest.php b/tests/Monolog/PsrLogCompatTest.php
index a4bbcd0c..eaeb3552 100644
--- a/tests/Monolog/PsrLogCompatTest.php
+++ b/tests/Monolog/PsrLogCompatTest.php
@@ -11,16 +11,21 @@
 
 namespace Monolog;
 
+use DateTimeZone;
 use Monolog\Handler\TestHandler;
 use Monolog\Formatter\LineFormatter;
 use Monolog\Processor\PsrLogMessageProcessor;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
 use Psr\Log\Test\LoggerInterfaceTest;
 
-class PsrLogCompatTest extends LoggerInterfaceTest
+class PsrLogCompatTest extends TestCase
 {
     private $handler;
 
-    public function getLogger()
+    public function getLogger(): LoggerInterface
     {
         $logger = new Logger('foo');
         $logger->pushHandler($handler = new TestHandler);
@@ -32,7 +37,7 @@ class PsrLogCompatTest extends LoggerInterfaceTest
         return $logger;
     }
 
-    public function getLogs()
+    public function getLogs(): array
     {
         $convert = function ($record) {
             $lower = function ($match) {
@@ -44,4 +49,123 @@ class PsrLogCompatTest extends LoggerInterfaceTest
 
         return array_map($convert, $this->handler->getRecords());
     }
+
+    public function testImplements()
+    {
+        $this->assertInstanceOf(LoggerInterface::class, $this->getLogger());
+    }
+
+    /**
+     * @dataProvider provideLevelsAndMessages
+     */
+    public function testLogsAtAllLevels($level, $message)
+    {
+        $logger = $this->getLogger();
+        $logger->{$level}($message, array('user' => 'Bob'));
+        $logger->log($level, $message, array('user' => 'Bob'));
+
+        $expected = array(
+            "$level message of level $level with context: Bob",
+            "$level message of level $level with context: Bob",
+        );
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function provideLevelsAndMessages()
+    {
+        return array(
+            LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
+            LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
+            LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
+            LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
+            LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
+            LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
+            LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
+            LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
+        );
+    }
+
+    public function testThrowsOnInvalidLevel()
+    {
+        $logger = $this->getLogger();
+
+        $this->expectException(InvalidArgumentException::class);
+        $logger->log('invalid level', 'Foo');
+    }
+
+    public function testContextReplacement()
+    {
+        $logger = $this->getLogger();
+        $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));
+
+        $expected = array('info {Message {nothing} Bob Bar a}');
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function testObjectCastToString()
+    {
+        $string = uniqid('DUMMY');
+        $dummy = $this->createStringable($string);
+        $dummy->expects($this->once())
+            ->method('__toString');
+
+        $this->getLogger()->warning($dummy);
+
+        $expected = array("warning $string");
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function testContextCanContainAnything()
+    {
+        $closed = fopen('php://memory', 'r');
+        fclose($closed);
+
+        $context = array(
+            'bool' => true,
+            'null' => null,
+            'string' => 'Foo',
+            'int' => 0,
+            'float' => 0.5,
+            'nested' => array('with object' => $this->createStringable()),
+            'object' => new \DateTime('now', new DateTimeZone('Europe/London')),
+            'resource' => fopen('php://memory', 'r'),
+            'closed' => $closed,
+        );
+
+        $this->getLogger()->warning('Crazy context data', $context);
+
+        $expected = array('warning Crazy context data');
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    public function testContextExceptionKeyCanBeExceptionOrOtherValues()
+    {
+        $logger = $this->getLogger();
+        $logger->warning('Random message', array('exception' => 'oops'));
+        $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));
+
+        $expected = array(
+            'warning Random message',
+            'critical Uncaught Exception!'
+        );
+        $this->assertEquals($expected, $this->getLogs());
+    }
+
+    /**
+     * Creates a mock of a `Stringable`.
+     *
+     * @param string $string The string that must be represented by the stringable.
+     * @return \PHPUnit_Framework_MockObject_MockObject A mock of an object that has a `__toString()` method.
+     */
+    protected function createStringable($string = '')
+    {
+        $mock = $this->getMockBuilder('Stringable')
+            ->setMethods(array('__toString'))
+            ->getMock();
+
+        $mock->method('__toString')
+            ->will($this->returnValue($string));
+
+        return $mock;
+    }
 }
diff --git a/tests/Monolog/UtilsTest.php b/tests/Monolog/UtilsTest.php
index bc216467..aced3baf 100644
--- a/tests/Monolog/UtilsTest.php
+++ b/tests/Monolog/UtilsTest.php
@@ -141,4 +141,49 @@ class UtilsTest extends \PHPUnit_Framework_TestCase
             [-1, 'UNDEFINED_ERROR'],
         ];
     }
+
+    public function provideIniValuesToConvertToBytes()
+    {
+        return [
+            ['1', 1],
+            ['2', 2],
+            ['2.5', 2],
+            ['2.9', 2],
+            ['1B', false],
+            ['1X', false],
+            ['1K', 1024],
+            ['1 K', 1024],
+            ['   5 M  ', 5*1024*1024],
+            ['1G', 1073741824],
+            ['', false],
+            [null, false],
+            ['A', false],
+            ['AA', false],
+            ['B', false],
+            ['BB', false],
+            ['G', false],
+            ['GG', false],
+            ['-1', -1],
+            ['-123', -123],
+            ['-1A', -1],
+            ['-1B', -1],
+            ['-123G', -123],
+            ['-B', false],
+            ['-A', false],
+            ['-', false],
+            [true, false],
+            [false, false],
+        ];
+    }
+
+    /**
+     * @dataProvider provideIniValuesToConvertToBytes
+     * @param mixed $input
+     * @param int|false $expected
+     */
+    public function testExpandIniShorthandBytes($input, $expected)
+    {
+        $result = Utils::expandIniShorthandBytes($input);
+        $this->assertEquals($expected, $result);
+    }
 }