Explorar el Código

Merge pull request #1083 from krisbuist/threshold-handler

Add an OverflowHandler
Jordi Boggiano hace 6 años
padre
commit
3a05a7c438

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

@@ -128,6 +128,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.
+- [_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 batch processing when you're only interested in significant 
+  failures instead of minor, single erroneous events. 
 
 ## Formatters
 

+ 127 - 0
src/Monolog/Handler/OverflowHandler.php

@@ -0,0 +1,127 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog\Handler;
+
+use Monolog\Logger;
+
+
+/**
+ * Handler to only pass log messages when a certain threshold of number of messages is reached.
+ *
+ * This can be useful in cases of processing a batch of data, but you're for example only interested
+ * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right?
+ *
+ * Usage example:
+ *
+ * ```
+ *   $log = new Logger('application');
+ *   $handler = new SomeHandler(...)
+ *
+ *   // Pass all warnings to the handler when more than 10 & all error messages when more then 5
+ *   $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]);
+ *
+ *   $log->pushHandler($overflow);
+ *```
+ *
+ * @author Kris Buist <krisbuist@gmail.com>
+ */
+class OverflowHandler 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 overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1
+            $this->thresholdMap[$level]--;
+            $this->buffer[$level][] = $record;
+            return false === $this->bubble;
+        }
+
+        if ($this->thresholdMap[$level] == 0) {
+            // This current message is breaking the threshold. Flush the buffer and continue handling the current record
+            foreach ($this->buffer[$level] ?? [] as $buffered) {
+                $this->handler->handle($buffered);
+            }
+            $this->thresholdMap[$level]--;
+            unset($this->buffer[$level]);
+        }
+
+        $this->handler->handle($record);
+
+        return false === $this->bubble;
+    }
+}

+ 108 - 0
tests/Monolog/Handler/OverflowHandlerTest.php

@@ -0,0 +1,108 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Test\TestCase;
+
+/**
+ * @author Kris Buist <krisbuist@gmail.com>
+ * @covers \Monolog\Handler\OverflowHandler
+ */
+class OverflowHandlerTest extends TestCase
+{
+    public function testNotPassingRecordsBeneathLogLevel()
+    {
+        $testHandler = new TestHandler();
+        $handler = new OverflowHandler($testHandler, [], Logger::INFO);
+        $handler->handle($this->getRecord(Logger::DEBUG));
+        $this->assertFalse($testHandler->hasDebugRecords());
+    }
+
+    public function testPassThroughWithoutThreshold()
+    {
+        $testHandler = new TestHandler();
+        $handler = new OverflowHandler($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 OverflowHandler($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 OverflowHandler($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());
+    }
+}