From fb2e21faa079715f55c60c0e866ea8425d7bc5a7 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Mon, 8 Jun 2026 10:58:11 +0200 Subject: [PATCH] fix(LogIterator): filter log levels to skip before JSON decoding - perform cheaper string check before returning current() item Signed-off-by: Maksim Sukharev --- lib/Log/LogIterator.php | 37 +++++++++++++++++++++++++++++++++- lib/Log/LogIteratorFactory.php | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/Log/LogIterator.php b/lib/Log/LogIterator.php index 89980d413..ecf8b3646 100644 --- a/lib/Log/LogIterator.php +++ b/lib/Log/LogIterator.php @@ -17,6 +17,10 @@ class LogIterator implements \Iterator { private $handle; private string $dateFormat; private \DateTimeZone $timezone; + /** + * @var int[]|null + */ + private ?array $levels; private string $buffer = ''; private string $lastLine = ''; @@ -29,11 +33,13 @@ class LogIterator implements \Iterator { * @param resource $handle * @param string $dateFormat * @param string $timezone + * @param int[]|null $levels Array of log levels to include, null to include all */ - public function __construct($handle, string $dateFormat, string $timezone) { + public function __construct($handle, string $dateFormat, string $timezone, ?array $levels = null) { $this->handle = $handle; $this->dateFormat = $dateFormat; $this->timezone = new \DateTimeZone($timezone); + $this->levels = $levels; $this->rewind(); } @@ -87,11 +93,22 @@ public function next(): void { } $this->lastLine = substr($this->buffer, $newlinePos + 1); $this->buffer = substr($this->buffer, 0, $newlinePos); + + // Skip lines that don't match the log level + if ($this->levels !== null && !$this->matchesLevelFilter($this->lastLine)) { + continue; + } + $this->currentKey++; return; } elseif ($this->position === 0) { $this->lastLine = $this->buffer; $this->buffer = ''; + + if ($this->levels !== null && !$this->matchesLevelFilter($this->lastLine)) { + return; + } + $this->currentKey++; return; } else { @@ -100,6 +117,24 @@ public function next(): void { } } + /** + * Check if a log line matches the allowed levels. + * Inaccurate check before full JSON decoding, + * CallbackFilterIterator still required to validate the entry. + */ + private function matchesLevelFilter(string $line): bool { + $levelPos = strpos($line, '"level":'); + if ($levelPos === false) { + return false; + } + $digit = substr($line, $levelPos + 8, 1); + if (!ctype_digit($digit)) { + return false; + } + $level = (int)$digit; + return in_array($level, $this->levels, true); + } + public function valid(): bool { if (!is_resource($this->handle)) { return false; diff --git a/lib/Log/LogIteratorFactory.php b/lib/Log/LogIteratorFactory.php index cc5d7af8f..03eadb856 100644 --- a/lib/Log/LogIteratorFactory.php +++ b/lib/Log/LogIteratorFactory.php @@ -36,7 +36,7 @@ public function getLogIterator(array $levels): \Iterator { if ($log instanceof IFileBased) { $handle = fopen($log->getLogFilePath(), 'rb'); if ($handle) { - $iterator = new LogIterator($handle, $dateFormat, $timezone); + $iterator = new LogIterator($handle, $dateFormat, $timezone, $levels); return new \CallbackFilterIterator($iterator, function ($logItem) use ($levels) { return $logItem && in_array($logItem['level'], $levels); });