1<?php declare(strict_types=1);
2
3/*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Monolog\Handler;
13
14use Monolog\Logger;
15use Monolog\Formatter\FormatterInterface;
16
17/**
18 * Handler to only pass log messages when a certain threshold of number of messages is reached.
19 *
20 * This can be useful in cases of processing a batch of data, but you're for example only interested
21 * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right?
22 *
23 * Usage example:
24 *
25 * ```
26 *   $log = new Logger('application');
27 *   $handler = new SomeHandler(...)
28 *
29 *   // Pass all warnings to the handler when more than 10 & all error messages when more then 5
30 *   $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]);
31 *
32 *   $log->pushHandler($overflow);
33 *```
34 *
35 * @author Kris Buist <krisbuist@gmail.com>
36 */
37class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface
38{
39    /** @var HandlerInterface */
40    private $handler;
41
42    /** @var int[] */
43    private $thresholdMap = [
44        Logger::DEBUG => 0,
45        Logger::INFO => 0,
46        Logger::NOTICE => 0,
47        Logger::WARNING => 0,
48        Logger::ERROR => 0,
49        Logger::CRITICAL => 0,
50        Logger::ALERT => 0,
51        Logger::EMERGENCY => 0,
52    ];
53
54    /**
55     * Buffer of all messages passed to the handler before the threshold was reached
56     *
57     * @var mixed[][]
58     */
59    private $buffer = [];
60
61    /**
62     * @param HandlerInterface $handler
63     * @param int[]            $thresholdMap Dictionary of logger level => threshold
64     */
65    public function __construct(
66        HandlerInterface $handler,
67        array $thresholdMap = [],
68        $level = Logger::DEBUG,
69        bool $bubble = true
70    ) {
71        $this->handler = $handler;
72        foreach ($thresholdMap as $thresholdLevel => $threshold) {
73            $this->thresholdMap[$thresholdLevel] = $threshold;
74        }
75        parent::__construct($level, $bubble);
76    }
77
78    /**
79     * Handles a record.
80     *
81     * All records may be passed to this method, and the handler should discard
82     * those that it does not want to handle.
83     *
84     * The return value of this function controls the bubbling process of the handler stack.
85     * Unless the bubbling is interrupted (by returning true), the Logger class will keep on
86     * calling further handlers in the stack with a given log record.
87     *
88     * {@inheritDoc}
89     */
90    public function handle(array $record): bool
91    {
92        if ($record['level'] < $this->level) {
93            return false;
94        }
95
96        $level = $record['level'];
97
98        if (!isset($this->thresholdMap[$level])) {
99            $this->thresholdMap[$level] = 0;
100        }
101
102        if ($this->thresholdMap[$level] > 0) {
103            // The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1
104            $this->thresholdMap[$level]--;
105            $this->buffer[$level][] = $record;
106
107            return false === $this->bubble;
108        }
109
110        if ($this->thresholdMap[$level] == 0) {
111            // This current message is breaking the threshold. Flush the buffer and continue handling the current record
112            foreach ($this->buffer[$level] ?? [] as $buffered) {
113                $this->handler->handle($buffered);
114            }
115            $this->thresholdMap[$level]--;
116            unset($this->buffer[$level]);
117        }
118
119        $this->handler->handle($record);
120
121        return false === $this->bubble;
122    }
123
124    /**
125     * {@inheritDoc}
126     */
127    public function setFormatter(FormatterInterface $formatter): HandlerInterface
128    {
129        if ($this->handler instanceof FormattableHandlerInterface) {
130            $this->handler->setFormatter($formatter);
131
132            return $this;
133        }
134
135        throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
136    }
137
138    /**
139     * {@inheritDoc}
140     */
141    public function getFormatter(): FormatterInterface
142    {
143        if ($this->handler instanceof FormattableHandlerInterface) {
144            return $this->handler->getFormatter();
145        }
146
147        throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
148    }
149}
150