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     * @param int|string       $level        The minimum logging level at which this handler will be triggered
65     * @param bool             $bubble
66     */
67    public function __construct(
68        HandlerInterface $handler,
69        array $thresholdMap = [],
70        $level = Logger::DEBUG,
71        bool $bubble = true
72    ) {
73        $this->handler = $handler;
74        foreach ($thresholdMap as $thresholdLevel => $threshold) {
75            $this->thresholdMap[$thresholdLevel] = $threshold;
76        }
77        parent::__construct($level, $bubble);
78    }
79
80    /**
81     * Handles a record.
82     *
83     * All records may be passed to this method, and the handler should discard
84     * those that it does not want to handle.
85     *
86     * The return value of this function controls the bubbling process of the handler stack.
87     * Unless the bubbling is interrupted (by returning true), the Logger class will keep on
88     * calling further handlers in the stack with a given log record.
89     *
90     * @param array $record The record to handle
91     *
92     * @return Boolean true means that this handler handled the record, and that bubbling is not permitted.
93     *                 false means the record was either not processed or that this handler allows bubbling.
94     */
95    public function handle(array $record): bool
96    {
97        if ($record['level'] < $this->level) {
98            return false;
99        }
100
101        $level = $record['level'];
102
103        if (!isset($this->thresholdMap[$level])) {
104            $this->thresholdMap[$level] = 0;
105        }
106
107        if ($this->thresholdMap[$level] > 0) {
108            // The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1
109            $this->thresholdMap[$level]--;
110            $this->buffer[$level][] = $record;
111
112            return false === $this->bubble;
113        }
114
115        if ($this->thresholdMap[$level] == 0) {
116            // This current message is breaking the threshold. Flush the buffer and continue handling the current record
117            foreach ($this->buffer[$level] ?? [] as $buffered) {
118                $this->handler->handle($buffered);
119            }
120            $this->thresholdMap[$level]--;
121            unset($this->buffer[$level]);
122        }
123
124        $this->handler->handle($record);
125
126        return false === $this->bubble;
127    }
128
129    /**
130     * {@inheritdoc}
131     */
132    public function setFormatter(FormatterInterface $formatter): HandlerInterface
133    {
134        if ($this->handler instanceof FormattableHandlerInterface) {
135            $this->handler->setFormatter($formatter);
136
137            return $this;
138        }
139
140        throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
141    }
142
143    /**
144     * {@inheritdoc}
145     */
146    public function getFormatter(): FormatterInterface
147    {
148        if ($this->handler instanceof FormattableHandlerInterface) {
149            return $this->handler->getFormatter();
150        }
151
152        throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.');
153    }
154}
155