From bd79504f1ee054855313df7d653f298f5060a0b0 Mon Sep 17 00:00:00 2001 From: Kris Buist Date: Wed, 15 Nov 2017 21:38:49 +0100 Subject: [PATCH 1/3] Add a ThresholdHandler This handler can be used to not let any messages from a certain lever through, unless they cross a configured threshold. This is useful together with, for example, a MailHandler when some batch processing is done and you're only interested in major failure and not a minor one. --- doc/02-handlers-formatters-processors.md | 4 + src/Monolog/Handler/ThresholdHandler.php | 127 ++++++++++++++++++ .../Monolog/Handler/ThresholdHandlerTest.php | 108 +++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 src/Monolog/Handler/ThresholdHandler.php create mode 100644 tests/Monolog/Handler/ThresholdHandlerTest.php diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index 9987908c..aeb77efd 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -125,6 +125,10 @@ has accessors to read out the information. - [_HandlerWrapper_](../src/Monolog/Handler/HandlerWrapper.php): A simple handler wrapper you can inherit from to create your own wrappers easily. +- [_ThresholdHandler_](../src/Monolog/Handler/ThresholdHandler.php): This handler will buffer all the log messages it + receives, up until a configured threshold is reached, after it will pass all log messages to the wrapped handler. + Useful for applying in bath processing when you're only interested in significant failures instead of minor, single + erroneous events. ## Formatters diff --git a/src/Monolog/Handler/ThresholdHandler.php b/src/Monolog/Handler/ThresholdHandler.php new file mode 100644 index 00000000..2b0949da --- /dev/null +++ b/src/Monolog/Handler/ThresholdHandler.php @@ -0,0 +1,127 @@ + + * + * 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; + + +/** + * Handler to only pass log messages when a certain threshold of messages is reached. + * + * This can be useful in cases of processing a batch of data, but you're for example only interested + * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right? + * + * Usage example: + * + * ``` + * $log = new Logger('application'); + * $handler = new SomeHandler(...) + * + * // Pass all warnings to the handler when more than 10 & all error messages when more then 5 + * $threshold = new ThresholdHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]); + * + * $log->pushHandler($threshold); + *``` + * + * @author Kris Buist + */ +class ThresholdHandler extends AbstractHandler +{ + /** @var HandlerInterface */ + private $handler; + + /** @var int[] */ + private $thresholdMap = [ + Logger::DEBUG => 0, + Logger::INFO => 0, + Logger::NOTICE => 0, + Logger::WARNING => 0, + Logger::ERROR => 0, + Logger::CRITICAL => 0, + Logger::ALERT => 0, + Logger::EMERGENCY => 0, + ]; + + /** + * Buffer of all messages passed to the handler before the threshold was reached + * + * @var mixed[][] + */ + private $buffer = []; + + /** + * @param HandlerInterface $handler + * @param int[] $thresholdMap Dictionary of logger level => threshold + * @param int $level + * @param bool $bubble + */ + public function __construct( + HandlerInterface $handler, + array $thresholdMap = [], + int $level = Logger::DEBUG, + bool $bubble = true + ) { + $this->handler = $handler; + foreach ($thresholdMap as $thresholdLevel => $threshold) { + $this->thresholdMap[$thresholdLevel] = $threshold; + } + parent::__construct($level, $bubble); + } + + /** + * Handles a record. + * + * All records may be passed to this method, and the handler should discard + * those that it does not want to handle. + * + * The return value of this function controls the bubbling process of the handler stack. + * Unless the bubbling is interrupted (by returning true), the Logger class will keep on + * calling further handlers in the stack with a given log record. + * + * @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. + */ + public function handle(array $record): bool + { + if ($record['level'] < $this->level) { + return false; + } + + $level = $record['level']; + + if (!isset($this->thresholdMap[$level])) { + $this->thresholdMap[$level] = 0; + } + + if ($this->thresholdMap[$level] > 0) { + // The threshold is not yet reached, so we're buffering the record and lowering the threshold by 1 + $this->thresholdMap[$level]--; + $this->buffer[$level][] = $record; + return false === $this->bubble; + } + + if ($this->thresholdMap[$level] == 0) { + // This current message is breaking the threshold. Flush the buffer and continue handling the current record + foreach ($this->buffer[$level] ?? [] as $buffered) { + $this->handler->handle($buffered); + } + $this->thresholdMap[$level]--; + unset($this->buffer[$level]); + } + + $this->handler->handle($record); + + return false === $this->bubble; + } +} diff --git a/tests/Monolog/Handler/ThresholdHandlerTest.php b/tests/Monolog/Handler/ThresholdHandlerTest.php new file mode 100644 index 00000000..4ad555d1 --- /dev/null +++ b/tests/Monolog/Handler/ThresholdHandlerTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Test\TestCase; + +/** + * @author Kris Buist + * @covers \Monolog\Handler\ThresholdHandler + */ +class ThresholdHandlerTest extends TestCase +{ + public function testNotPassingRecordsBeneathLogLevel() + { + $testHandler = new TestHandler(); + $handler = new ThresholdHandler($testHandler, [], Logger::INFO); + $handler->handle($this->getRecord(Logger::DEBUG)); + $this->assertFalse($testHandler->hasDebugRecords()); + } + + public function testPassThroughWithoutThreshold() + { + $testHandler = new TestHandler(); + $handler = new ThresholdHandler($testHandler, [], Logger::INFO); + + $handler->handle($this->getRecord(Logger::INFO, 'Info 1')); + $handler->handle($this->getRecord(Logger::INFO, 'Info 2')); + $handler->handle($this->getRecord(Logger::WARNING, 'Warning 1')); + + $this->assertTrue($testHandler->hasInfoThatContains('Info 1')); + $this->assertTrue($testHandler->hasInfoThatContains('Info 2')); + $this->assertTrue($testHandler->hasWarningThatContains('Warning 1')); + } + + /** + * @test + */ + public function testHoldingMessagesBeneathThreshold() + { + $testHandler = new TestHandler(); + $handler = new ThresholdHandler($testHandler, [Logger::INFO => 3]); + + $handler->handle($this->getRecord(Logger::DEBUG, 'debug 1')); + $handler->handle($this->getRecord(Logger::DEBUG, 'debug 2')); + + foreach (range(1, 3) as $i) { + $handler->handle($this->getRecord(Logger::INFO, 'info ' . $i)); + } + + $this->assertTrue($testHandler->hasDebugThatContains('debug 1')); + $this->assertTrue($testHandler->hasDebugThatContains('debug 2')); + $this->assertFalse($testHandler->hasInfoRecords()); + + $handler->handle($this->getRecord(Logger::INFO, 'info 4')); + + foreach (range(1, 4) as $i) { + $this->assertTrue($testHandler->hasInfoThatContains('info ' . $i)); + } + } + + /** + * @test + */ + public function testCombinedThresholds() + { + $testHandler = new TestHandler(); + $handler = new ThresholdHandler($testHandler, [Logger::INFO => 5, Logger::WARNING => 10]); + + $handler->handle($this->getRecord(Logger::DEBUG)); + + foreach (range(1, 5) as $i) { + $handler->handle($this->getRecord(Logger::INFO, 'info ' . $i)); + } + + foreach (range(1, 10) as $i) { + $handler->handle($this->getRecord(Logger::WARNING, 'warning ' . $i)); + } + + // Only 1 DEBUG records + $this->assertCount(1, $testHandler->getRecords()); + + $handler->handle($this->getRecord(Logger::INFO, 'info final')); + + // 1 DEBUG + 5 buffered INFO + 1 new INFO + $this->assertCount(7, $testHandler->getRecords()); + + $handler->handle($this->getRecord(Logger::WARNING, 'warning final')); + + // 1 DEBUG + 6 INFO + 10 buffered WARNING + 1 new WARNING + $this->assertCount(18, $testHandler->getRecords()); + + $handler->handle($this->getRecord(Logger::INFO, 'Another info')); + $handler->handle($this->getRecord(Logger::WARNING, 'Anther warning')); + + // 1 DEBUG + 6 INFO + 11 WARNING + 1 new INFO + 1 new WARNING + $this->assertCount(20, $testHandler->getRecords()); + } +} From 7dd453e6944a4c65bdeedff47c54d95d8f72c8cb Mon Sep 17 00:00:00 2001 From: Kris Buist Date: Tue, 20 Nov 2018 20:03:58 +0100 Subject: [PATCH 2/3] Rename ThresholdHandler to OverflowHandler --- doc/02-handlers-formatters-processors.md | 8 ++++---- .../{ThresholdHandler.php => OverflowHandler.php} | 10 +++++----- ...esholdHandlerTest.php => OverflowHandlerTest.php} | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) rename src/Monolog/Handler/{ThresholdHandler.php => OverflowHandler.php} (91%) rename tests/Monolog/Handler/{ThresholdHandlerTest.php => OverflowHandlerTest.php} (88%) diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index aeb77efd..c5f3abac 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -125,10 +125,10 @@ has accessors to read out the information. - [_HandlerWrapper_](../src/Monolog/Handler/HandlerWrapper.php): A simple handler wrapper you can inherit from to create your own wrappers easily. -- [_ThresholdHandler_](../src/Monolog/Handler/ThresholdHandler.php): This handler will buffer all the log messages it - receives, up until a configured threshold is reached, after it will pass all log messages to the wrapped handler. - Useful for applying in bath processing when you're only interested in significant failures instead of minor, single - erroneous events. +- [_OverflowHandler_](../src/Monolog/Handler/OverflowHandler.php): This handler will buffer all the log messages it + receives, up until a configured threshold of number of messages of a certain lever is reached, after it will pass all + log messages to the wrapped handler. Useful for applying in bath processing when you're only interested in significant + failures instead of minor, single erroneous events. ## Formatters diff --git a/src/Monolog/Handler/ThresholdHandler.php b/src/Monolog/Handler/OverflowHandler.php similarity index 91% rename from src/Monolog/Handler/ThresholdHandler.php rename to src/Monolog/Handler/OverflowHandler.php index 2b0949da..30e81c43 100644 --- a/src/Monolog/Handler/ThresholdHandler.php +++ b/src/Monolog/Handler/OverflowHandler.php @@ -15,7 +15,7 @@ use Monolog\Logger; /** - * Handler to only pass log messages when a certain threshold of messages is reached. + * Handler to only pass log messages when a certain threshold of number of messages is reached. * * This can be useful in cases of processing a batch of data, but you're for example only interested * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right? @@ -27,14 +27,14 @@ use Monolog\Logger; * $handler = new SomeHandler(...) * * // Pass all warnings to the handler when more than 10 & all error messages when more then 5 - * $threshold = new ThresholdHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]); + * $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]); * - * $log->pushHandler($threshold); + * $log->pushHandler($overflow); *``` * * @author Kris Buist */ -class ThresholdHandler extends AbstractHandler +class OverflowHandler extends AbstractHandler { /** @var HandlerInterface */ private $handler; @@ -105,7 +105,7 @@ class ThresholdHandler extends AbstractHandler } if ($this->thresholdMap[$level] > 0) { - // The threshold is not yet reached, so we're buffering the record and lowering the threshold by 1 + // The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1 $this->thresholdMap[$level]--; $this->buffer[$level][] = $record; return false === $this->bubble; diff --git a/tests/Monolog/Handler/ThresholdHandlerTest.php b/tests/Monolog/Handler/OverflowHandlerTest.php similarity index 88% rename from tests/Monolog/Handler/ThresholdHandlerTest.php rename to tests/Monolog/Handler/OverflowHandlerTest.php index 4ad555d1..c62f441a 100644 --- a/tests/Monolog/Handler/ThresholdHandlerTest.php +++ b/tests/Monolog/Handler/OverflowHandlerTest.php @@ -16,14 +16,14 @@ use Monolog\Test\TestCase; /** * @author Kris Buist - * @covers \Monolog\Handler\ThresholdHandler + * @covers \Monolog\Handler\OverflowHandler */ -class ThresholdHandlerTest extends TestCase +class OverflowHandlerTest extends TestCase { public function testNotPassingRecordsBeneathLogLevel() { $testHandler = new TestHandler(); - $handler = new ThresholdHandler($testHandler, [], Logger::INFO); + $handler = new OverflowHandler($testHandler, [], Logger::INFO); $handler->handle($this->getRecord(Logger::DEBUG)); $this->assertFalse($testHandler->hasDebugRecords()); } @@ -31,7 +31,7 @@ class ThresholdHandlerTest extends TestCase public function testPassThroughWithoutThreshold() { $testHandler = new TestHandler(); - $handler = new ThresholdHandler($testHandler, [], Logger::INFO); + $handler = new OverflowHandler($testHandler, [], Logger::INFO); $handler->handle($this->getRecord(Logger::INFO, 'Info 1')); $handler->handle($this->getRecord(Logger::INFO, 'Info 2')); @@ -48,7 +48,7 @@ class ThresholdHandlerTest extends TestCase public function testHoldingMessagesBeneathThreshold() { $testHandler = new TestHandler(); - $handler = new ThresholdHandler($testHandler, [Logger::INFO => 3]); + $handler = new OverflowHandler($testHandler, [Logger::INFO => 3]); $handler->handle($this->getRecord(Logger::DEBUG, 'debug 1')); $handler->handle($this->getRecord(Logger::DEBUG, 'debug 2')); @@ -74,7 +74,7 @@ class ThresholdHandlerTest extends TestCase public function testCombinedThresholds() { $testHandler = new TestHandler(); - $handler = new ThresholdHandler($testHandler, [Logger::INFO => 5, Logger::WARNING => 10]); + $handler = new OverflowHandler($testHandler, [Logger::INFO => 5, Logger::WARNING => 10]); $handler->handle($this->getRecord(Logger::DEBUG)); From c9a4e5fb48aa27a6b31cec6692cb64f77732c3e0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 30 Jun 2019 18:35:22 +0200 Subject: [PATCH 3/3] Fix typo --- doc/02-handlers-formatters-processors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index c5f3abac..5e08d077 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -127,7 +127,7 @@ your own wrappers easily. - [_OverflowHandler_](../src/Monolog/Handler/OverflowHandler.php): This handler will buffer all the log messages it receives, up until a configured threshold of number of messages of a certain lever is reached, after it will pass all - log messages to the wrapped handler. Useful for applying in bath processing when you're only interested in significant + log messages to the wrapped handler. Useful for applying in batch processing when you're only interested in significant failures instead of minor, single erroneous events. ## Formatters