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\Handler\FingersCrossed\ErrorLevelActivationStrategy;
15use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
16use Monolog\Logger;
17use Monolog\ResettableInterface;
18use Monolog\Formatter\FormatterInterface;
19use Psr\Log\LogLevel;
20
21/**
22 * Buffers all records until a certain level is reached
23 *
24 * The advantage of this approach is that you don't get any clutter in your log files.
25 * Only requests which actually trigger an error (or whatever your actionLevel is) will be
26 * in the logs, but they will contain all records, not only those above the level threshold.
27 *
28 * You can then have a passthruLevel as well which means that at the end of the request,
29 * even if it did not get activated, it will still send through log records of e.g. at least a
30 * warning level.
31 *
32 * You can find the various activation strategies in the
33 * Monolog\Handler\FingersCrossed\ namespace.
34 *
35 * @author Jordi Boggiano <j.boggiano@seld.be>
36 *
37 * @phpstan-import-type Record from \Monolog\Logger
38 * @phpstan-import-type Level from \Monolog\Logger
39 * @phpstan-import-type LevelName from \Monolog\Logger
40 */
41class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface
42{
43    use ProcessableHandlerTrait;
44
45    /**
46     * @var callable|HandlerInterface
47     * @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface
48     */
49    protected $handler;
50    /** @var ActivationStrategyInterface */
51    protected $activationStrategy;
52    /** @var bool */
53    protected $buffering = true;
54    /** @var int */
55    protected $bufferSize;
56    /** @var Record[] */
57    protected $buffer = [];
58    /** @var bool */
59    protected $stopBuffering;
60    /**
61     * @var ?int
62     * @phpstan-var ?Level
63     */
64    protected $passthruLevel;
65    /** @var bool */
66    protected $bubble;
67
68    /**
69     * @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler
70     *
71     * @param callable|HandlerInterface              $handler            Handler or factory callable($record|null, $fingersCrossedHandler).
72     * @param int|string|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action, or a level name/value at which the handler is activated
73     * @param int                                    $bufferSize         How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
74     * @param bool                                   $bubble             Whether the messages that are handled can bubble up the stack or not
75     * @param bool                                   $stopBuffering      Whether the handler should stop buffering after being triggered (default true)
76     * @param int|string                             $passthruLevel      Minimum level to always flush to handler on close, even if strategy not triggered
77     *
78     * @phpstan-param Level|LevelName|LogLevel::* $passthruLevel
79     * @phpstan-param Level|LevelName|LogLevel::*|ActivationStrategyInterface $activationStrategy
80     */
81    public function __construct($handler, $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, $passthruLevel = null)
82    {
83        if (null === $activationStrategy) {
84            $activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING);
85        }
86
87        // convert simple int activationStrategy to an object
88        if (!$activationStrategy instanceof ActivationStrategyInterface) {
89            $activationStrategy = new ErrorLevelActivationStrategy($activationStrategy);
90        }
91
92        $this->handler = $handler;
93        $this->activationStrategy = $activationStrategy;
94        $this->bufferSize = $bufferSize;
95        $this->bubble = $bubble;
96        $this->stopBuffering = $stopBuffering;
97
98        if ($passthruLevel !== null) {
99            $this->passthruLevel = Logger::toMonologLevel($passthruLevel);
100        }
101
102        if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
103            throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
104        }
105    }
106
107    /**
108     * {@inheritDoc}
109     */
110    public function isHandling(array $record): bool
111    {
112        return true;
113    }
114
115    /**
116     * Manually activate this logger regardless of the activation strategy
117     */
118    public function activate(): void
119    {
120        if ($this->stopBuffering) {
121            $this->buffering = false;
122        }
123
124        $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
125        $this->buffer = [];
126    }
127
128    /**
129     * {@inheritDoc}
130     */
131    public function handle(array $record): bool
132    {
133        if ($this->processors) {
134            /** @var Record $record */
135            $record = $this->processRecord($record);
136        }
137
138        if ($this->buffering) {
139            $this->buffer[] = $record;
140            if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) {
141                array_shift($this->buffer);
142            }
143            if ($this->activationStrategy->isHandlerActivated($record)) {
144                $this->activate();
145            }
146        } else {
147            $this->getHandler($record)->handle($record);
148        }
149
150        return false === $this->bubble;
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    public function close(): void
157    {
158        $this->flushBuffer();
159
160        $this->getHandler()->close();
161    }
162
163    public function reset()
164    {
165        $this->flushBuffer();
166
167        $this->resetProcessors();
168
169        if ($this->getHandler() instanceof ResettableInterface) {
170            $this->getHandler()->reset();
171        }
172    }
173
174    /**
175     * Clears the buffer without flushing any messages down to the wrapped handler.
176     *
177     * It also resets the handler to its initial buffering state.
178     */
179    public function clear(): void
180    {
181        $this->buffer = [];
182        $this->reset();
183    }
184
185    /**
186     * Resets the state of the handler. Stops forwarding records to the wrapped handler.
187     */
188    private function flushBuffer(): void
189    {
190        if (null !== $this->passthruLevel) {
191            $level = $this->passthruLevel;
192            $this->buffer = array_filter($this->buffer, function ($record) use ($level) {
193                return $record['level'] >= $level;
194            });
195            if (count($this->buffer) > 0) {
196                $this->getHandler(end($this->buffer))->handleBatch($this->buffer);
197            }
198        }
199
200        $this->buffer = [];
201        $this->buffering = true;
202    }
203
204    /**
205     * Return the nested handler
206     *
207     * If the handler was provided as a factory callable, this will trigger the handler's instantiation.
208     *
209     * @return HandlerInterface
210     *
211     * @phpstan-param Record $record
212     */
213    public function getHandler(array $record = null)
214    {
215        if (!$this->handler instanceof HandlerInterface) {
216            $this->handler = ($this->handler)($record, $this);
217            if (!$this->handler instanceof HandlerInterface) {
218                throw new \RuntimeException("The factory callable should return a HandlerInterface");
219            }
220        }
221
222        return $this->handler;
223    }
224
225    /**
226     * {@inheritDoc}
227     */
228    public function setFormatter(FormatterInterface $formatter): HandlerInterface
229    {
230        $handler = $this->getHandler();
231        if ($handler instanceof FormattableHandlerInterface) {
232            $handler->setFormatter($formatter);
233
234            return $this;
235        }
236
237        throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
238    }
239
240    /**
241     * {@inheritDoc}
242     */
243    public function getFormatter(): FormatterInterface
244    {
245        $handler = $this->getHandler();
246        if ($handler instanceof FormattableHandlerInterface) {
247            return $handler->getFormatter();
248        }
249
250        throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.');
251    }
252}
253