src/CallbackFilterHandler.php
<?php
/**
* This file is part of the mimmi20/monolog-callbackfilterhandler package.
*
* Copyright (c) 2022-2024, Thomas Mueller <mimmi20@live.de>
* Copyright (c) 2015-2021, Laurent Laville <pear@laurent-laville.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types = 1);
/**
* Callback Filter Handler for Monolog.
*/
namespace Mimmi20\Monolog\Handler;
use Closure;
use JsonException;
use Monolog\Handler\AbstractHandler;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\ProcessableHandlerInterface;
use Monolog\Handler\ProcessableHandlerTrait;
use Monolog\Level;
use Monolog\LogRecord;
use Monolog\ResettableInterface;
use Override;
use Psr\Log\LogLevel;
use RuntimeException;
use function count;
use function json_encode;
use function sprintf;
use const JSON_THROW_ON_ERROR;
/**
* Monolog handler wrapper that filters records based on a list of callback functions.
*/
final class CallbackFilterHandler extends AbstractHandler implements ProcessableHandlerInterface
{
use ProcessableHandlerTrait;
/**
* Filters Closure to restrict log records.
*
* @var array<Closure>
* @phpstan-var array<int|string, (Closure(LogRecord, Level): bool)>
*/
private array $filters;
/**
* @param Closure|HandlerInterface $handler handler or factory Closure($record, $this)
* @param array<Closure> $filters A list of filters to apply
* @param int|Level|string $level The minimum logging level at which this handler will be triggered
* @param bool $bubble Whether the messages that are handled can bubble up the stack or not
* @phpstan-param (Closure(LogRecord|null, HandlerInterface): HandlerInterface)|HandlerInterface $handler
* @phpstan-param array<int|string, (Closure(LogRecord, Level): bool)> $filters
* @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level
*
* @throws RuntimeException
*/
public function __construct(
private Closure | HandlerInterface $handler,
array $filters,
int | string | Level $level = Level::Debug,
bool $bubble = true,
) {
parent::__construct($level, $bubble);
$this->filters = [];
foreach ($filters as $filter) {
if (!$filter instanceof Closure) {
try {
throw new RuntimeException(
sprintf(
'The given filter (%s) is not a Closure',
json_encode($filter, JSON_THROW_ON_ERROR),
),
);
} catch (JsonException $e) {
throw new RuntimeException('The given filter is not a Closure', 0, $e);
}
}
$this->filters[] = $filter;
}
}
/** @throws void */
#[Override]
public function isHandling(LogRecord $record): bool
{
if (!parent::isHandling($record)) {
return false;
}
if (isset($record->message)) {
// when record is fulfilled, try each filter
foreach ($this->filters as $filter) {
if (!$filter($record, $this->level)) {
return false;
}
}
}
return true;
}
/** @throws RuntimeException */
#[Override]
public function handle(LogRecord $record): bool
{
// The same logic as in FilterHandler
if (!$this->isHandling($record)) {
return false;
}
$record = $this->processRecord($record);
$this->getHandler($record)->handle($record);
return $this->bubble === false;
}
/**
* @param array<LogRecord> $records
*
* @throws RuntimeException
*/
#[Override]
public function handleBatch(array $records): void
{
// The same logic as in FilterHandler
$filtered = [];
foreach ($records as $record) {
if (!$this->isHandling($record)) {
continue;
}
$filtered[] = $record;
}
if (0 >= count($filtered)) {
return;
}
$this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered);
}
/**
* Return the nested handler
*
* If the handler was provided as a factory, this will trigger the handler's instantiation.
*
* @throws RuntimeException
*/
public function getHandler(LogRecord | null $record = null): HandlerInterface
{
// The same logic as in FingersCrossedHandler
if (!$this->handler instanceof HandlerInterface) {
$handler = ($this->handler)($record, $this);
if (!$handler instanceof HandlerInterface) {
throw new RuntimeException('The factory Closure should return a HandlerInterface');
}
$this->handler = $handler;
}
return $this->handler;
}
/** @throws RuntimeException */
#[Override]
public function reset(): void
{
$this->resetProcessors();
$handler = $this->getHandler();
if (!$handler instanceof ResettableInterface) {
return;
}
$handler->reset();
}
}