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