From bbc4ec2e93554324546f1612a177a89c73a87e6b Mon Sep 17 00:00:00 2001 From: Beau Hastings Date: Wed, 29 Oct 2025 09:22:45 -0500 Subject: [PATCH] fix: log level parsing to prevent false matches in message text Replace strpos() check with strict regex that only matches log levels when they appear in the correct position after the timestamp bracket. This prevents words like "processed" or "failed" in log message text from being incorrectly identified as log levels. Signed-off-by: Beau Hastings --- .../LaravelLogViewer/LaravelLogViewer.php | 5 +- tests/LaravelLogViewerTest.php | 72 ++++++++++++++++--- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php b/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php index 4325026..b5c8dea 100644 --- a/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php +++ b/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php @@ -184,8 +184,9 @@ public function all() foreach ($headings as $h) { for ($i = 0, $j = count($h); $i < $j; $i++) { foreach ($this->level->all() as $level) { - if (strpos(strtolower($h[$i]), '.' . $level) || strpos(strtolower($h[$i]), $level . ':')) { - + // Check if level appears in correct position (after context or right after timestamp) + $levelPattern = '/\]\s*(?:\w+\.)?' . preg_quote($level, '/') . ':/i'; + if (preg_match($levelPattern, $h[$i])) { preg_match($this->pattern->getPattern('current_log', 0) . $level . $this->pattern->getPattern('current_log', 1), $h[$i], $current); if (!isset($current[4])) { continue; diff --git a/tests/LaravelLogViewerTest.php b/tests/LaravelLogViewerTest.php index 3de7b4a..a379ed0 100644 --- a/tests/LaravelLogViewerTest.php +++ b/tests/LaravelLogViewerTest.php @@ -48,24 +48,24 @@ public function testSetFolderWithCorrectPath() public function testSetFolderWithArrayStoragePath() { $path = __DIR__; - + $laravel_log_viewer = new LaravelLogViewer(); $laravel_log_viewer->setStoragePath([$path]); if(!File::exists("$path/samuel")) File::makeDirectory("$path/samuel"); $laravel_log_viewer->setFolder('samuel'); - + $this->assertEquals("samuel", $laravel_log_viewer->getFolderName()); } public function testSetFolderWithDefaultStoragePath() { - + $laravel_log_viewer = new LaravelLogViewer(); $laravel_log_viewer->setStoragePath(storage_path()); $laravel_log_viewer->setFolder('logs'); - + $this->assertEquals("logs", $laravel_log_viewer->getFolderName()); } @@ -84,7 +84,7 @@ public function testPathToLogFile() $laravel_log_viewer = new LaravelLogViewer(); $pathToLogFile = $laravel_log_viewer->pathToLogFile(storage_path(('logs/laravel.log'))); - + $this->assertEquals($pathToLogFile, storage_path('logs/laravel.log')); } @@ -126,7 +126,7 @@ public function testAllWithEmptyFileName() { $laravel_log_viewer = new LaravelLogViewer(); $laravel_log_viewer->setStoragePath(__DIR__); - + $data = $laravel_log_viewer->all(); $this->assertEquals('local', $data[0]['context']); $this->assertEquals('error', $data[0]['level']); @@ -144,7 +144,7 @@ public function testFolderFiles() $this->assertIsArray($data); $this->assertNotEmpty($data); - + $this->assertStringContainsString('tests', $data[count(explode($data[0], '/')) - 1]); } @@ -153,7 +153,7 @@ public function testGetFolderFiles() $laravel_log_viewer = new LaravelLogViewer(); $laravel_log_viewer->setStoragePath(__DIR__); $data = $laravel_log_viewer->getFolderFiles(); - + $this->assertIsArray($data); $this->assertNotEmpty($data, "Folder files is null"); } @@ -163,7 +163,7 @@ public function testGetFiles() $laravel_log_viewer = new LaravelLogViewer(); $laravel_log_viewer->setStoragePath(storage_path()); $data = $laravel_log_viewer->getFiles(); - + $this->assertIsArray($data); $this->assertNotEmpty($data, "Folder files is null"); } @@ -173,7 +173,7 @@ public function testGetFolders() $laravel_log_viewer = new LaravelLogViewer(); $laravel_log_viewer->setStoragePath(storage_path()); $data = $laravel_log_viewer->getFolders(); - + $this->assertIsArray($data); $this->assertNotEmpty($data, "files is null"); } @@ -184,10 +184,60 @@ public function testDirectoryStructure() ob_start(); $log_viewer->directoryTreeStructure(storage_path('logs'), $log_viewer->foldersAndFiles()); $data = ob_get_clean(); - + $this->assertIsString($data); $this->assertNotEmpty($data); } + /** + * Test that level names appearing in log message text are not incorrectly + * parsed as separate log levels. This tests the fix for the bug where + * words like "processed" or "failed" in messages were creating false log entries. + */ + public function testLevelNamesInMessageTextNotParsedAsLevels() + { + // Create a temporary log file with entries containing level names in the message + $testLogPath = storage_path('logs/test-level-parsing.log'); + $testLogContent = <<<'LOG' +[2025-10-28 20:59:22] local.INFO: Redirect map successfully rebuilt and cached. Entries processed: 6866, Aliases mapped: 15918 +[2025-10-28 20:59:23] local.WARNING: Job processing failed with error code 500 +[2025-10-28 20:59:24] local.DEBUG: Processing diagnostic information +LOG; + + File::put($testLogPath, $testLogContent); + + try { + $laravel_log_viewer = new LaravelLogViewer(); + $laravel_log_viewer->setFile($testLogPath); + $data = $laravel_log_viewer->all(); + + // Should only have 3 log entries (not more due to false "processed" or "failed" matches) + $this->assertCount(3, $data, 'Should only parse 3 log entries, not create false entries from level names in message text'); + + // Verify the first entry is correctly parsed as INFO level, not "processed" + $this->assertEquals('info', $data[2]['level'], 'First entry should be INFO level'); + $this->assertEquals('local', $data[2]['context']); + $this->assertStringContainsString('Entries processed: 6866', $data[2]['text'], 'Message text should contain "processed" but not be parsed as a level'); + + // Verify the second entry is correctly parsed as WARNING level, not "failed" + $this->assertEquals('warning', $data[1]['level'], 'Second entry should be WARNING level'); + $this->assertEquals('local', $data[1]['context']); + $this->assertStringContainsString('Job processing failed', $data[1]['text'], 'Message text should contain "failed" but not be parsed as a level'); + + // Verify the third entry is correctly parsed as DEBUG level + $this->assertEquals('debug', $data[0]['level'], 'Third entry should be DEBUG level'); + + // Ensure no entries have "processed" or "failed" as their level + foreach ($data as $entry) { + $this->assertNotEquals('processed', $entry['level'], 'No entry should have "processed" as a level'); + $this->assertNotEquals('failed', $entry['level'], 'No entry should have "failed" as a level'); + } + } finally { + // Clean up the test log file + if (File::exists($testLogPath)) { + File::delete($testLogPath); + } + } + } }