From be6dff5b4ed55dd55fe6d2f606bcec13a11ef39c Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sun, 14 Dec 2025 06:10:18 +0330 Subject: [PATCH 1/3] Fix log file resolution for stack logging driver --- src/Concerns/ReadsLogs.php | 76 +++++- .../Feature/Mcp/Tools/ReadLogEntriesTest.php | 238 ++++++++++++++++++ 2 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/Mcp/Tools/ReadLogEntriesTest.php diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index 0b607ad1..efc3082d 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -45,11 +45,83 @@ protected function resolveLogFilePath(): string $channel = Config::get('logging.default'); $channelConfig = Config::get("logging.channels.{$channel}"); + // Handle stack driver by resolving to its first channel with a path + $channelConfig = $this->resolveChannelWithPath($channelConfig); + if (($channelConfig['driver'] ?? null) === 'daily') { - return storage_path('logs/laravel-'.date('Y-m-d').'.log'); + return $this->resolveDailyLogFilePath($channelConfig['path'] ?? storage_path('logs/laravel.log')); + } + + return $channelConfig['path'] ?? storage_path('logs/laravel.log'); + } + + /** + * Resolve a channel config that has a path, handling stack drivers recursively. + * + * @param array|null $channelConfig + * @return array|null + */ + protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0): ?array + { + if ($channelConfig === null || $depth > 5) { + return $channelConfig; + } + + if (($channelConfig['driver'] ?? null) !== 'stack') { + return $channelConfig; + } + + $stackChannels = $channelConfig['channels'] ?? []; + + foreach ($stackChannels as $stackChannel) { + $stackChannelConfig = Config::get("logging.channels.{$stackChannel}"); + + if (! is_array($stackChannelConfig)) { + continue; + } + + $resolved = $this->resolveChannelWithPath($stackChannelConfig, $depth + 1); + + if (isset($resolved['path'])) { + return $resolved; + } + } + + return $channelConfig; + } + + /** + * Resolve the daily log file path, falling back to the most recent if today's doesn't exist. + * + * @param string $basePath The configured path (e.g., storage_path('logs/laravel.log')) + */ + protected function resolveDailyLogFilePath(string $basePath): string + { + // Daily driver appends date before the extension: laravel.log -> laravel-2025-12-14.log + $pathInfo = pathinfo($basePath); + $directory = $pathInfo['dirname']; + $filename = $pathInfo['filename']; + $extension = isset($pathInfo['extension']) ? '.'.$pathInfo['extension'] : ''; + + $todayLogFile = $directory.DIRECTORY_SEPARATOR.$filename.'-'.date('Y-m-d').$extension; + + if (file_exists($todayLogFile)) { + return $todayLogFile; + } + + // Look for the most recent daily log file with matching base name + $pattern = $directory.DIRECTORY_SEPARATOR.$filename.'-*'.$extension; + $files = glob($pattern); + + if ($files !== false && $files !== []) { + // Sort by filename (which includes date) in descending order to get most recent + rsort($files); + + return $files[0]; } - return storage_path('logs/laravel.log'); + // Fall back to today's path even if it doesn't exist (error will be handled by caller) + return $todayLogFile; } /** diff --git a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php new file mode 100644 index 00000000..5c95d362 --- /dev/null +++ b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php @@ -0,0 +1,238 @@ + 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: First log message +[2024-01-15 10:01:00] local.ERROR: Error occurred +[2024-01-15 10:02:00] local.WARNING: Warning message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 2])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.WARNING: Warning message', 'local.ERROR: Error occurred') + ->toolTextDoesNotContain('local.DEBUG: First log message'); +}); + +test('it detects daily driver directly and reads configured path', function (): void { + $basePath = storage_path('logs/laravel.log'); + $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Daily log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Daily log message'); +}); + +test('it detects daily driver within stack channel', function (): void { + $basePath = storage_path('logs/laravel.log'); + $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['daily'], + ]); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Stack with daily log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Stack with daily log message'); +}); + +test('it uses custom path from daily channel config', function (): void { + $basePath = storage_path('logs/custom-app.log'); + $logFile = storage_path('logs/custom-app-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Custom path log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Custom path log message'); +}); + +test('it falls back to most recent daily log when today has no logs', function (): void { + $basePath = storage_path('logs/laravel.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + $logDir = storage_path('logs'); + File::ensureDirectoryExists($logDir); + + // Create a log file for yesterday + $yesterdayLogFile = $logDir.'/laravel-'.date('Y-m-d', strtotime('-1 day')).'.log'; + + $logContent = <<<'LOG' +[2024-01-14 10:00:00] local.DEBUG: Yesterday's log message +LOG; + + File::put($yesterdayLogFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Yesterday\'s log message'); +}); + +test('it uses single channel path from stack when no daily channel', function (): void { + $logFile = storage_path('logs/app.log'); + + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['single'], + ]); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Single in stack log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Single in stack log message'); +}); + +test('it returns error when entries argument is invalid', function (): void { + $tool = new ReadLogEntries; + + // Test with zero + $response = $tool->handle(new Request(['entries' => 0])); + expect($response)->isToolResult() + ->toolHasError() + ->toolTextContains('The "entries" argument must be greater than 0.'); + + // Test with negative + $response = $tool->handle(new Request(['entries' => -5])); + expect($response)->isToolResult() + ->toolHasError() + ->toolTextContains('The "entries" argument must be greater than 0.'); +}); + +test('it returns error when log file does not exist', function (): void { + Config::set('logging.default', 'single'); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + ]); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 10])); + + expect($response)->isToolResult() + ->toolHasError() + ->toolTextContains('Log file not found'); +}); + +test('it returns error when log file is empty', function (): void { + $logFile = storage_path('logs/laravel.log'); + + Config::set('logging.default', 'single'); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + File::put($logFile, ''); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 5])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Unable to retrieve log entries, or no entries yet.'); +}); From c82c4fde43f87581d24ee0f67922000b4a0bcbd1 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 17 Dec 2025 18:47:35 +0530 Subject: [PATCH 2/3] test: add test for filtering Signed-off-by: Pushpak Chhajed --- .../Feature/Mcp/Tools/ReadLogEntriesTest.php | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php index 5c95d362..fc6d5533 100644 --- a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php +++ b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php @@ -8,7 +8,6 @@ use Laravel\Mcp\Request; beforeEach(function (): void { - // Clean up any existing log files before each test $logDir = storage_path('logs'); $files = glob($logDir.'/*.log'); if ($files) { @@ -154,7 +153,7 @@ expect($response)->isToolResult() ->toolHasNoError() - ->toolTextContains('local.DEBUG: Yesterday\'s log message'); + ->toolTextContains("local.DEBUG: Yesterday's log message"); }); test('it uses single channel path from stack when no daily channel', function (): void { @@ -236,3 +235,31 @@ ->toolHasNoError() ->toolTextContains('Unable to retrieve log entries, or no entries yet.'); }); + +test('it ignores non-daily log files when selecting most recent daily log', function (): void { + $basePath = storage_path('logs/laravel.log'); + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + $logDir = storage_path('logs'); + File::ensureDirectoryExists($logDir); + + File::put($logDir.'/laravel-2024-01-10.log', '[2024-01-10 10:00:00] local.DEBUG: Daily log from 2024-01-10'); + File::put($logDir.'/laravel-2024-01-15.log', '[2024-01-15 10:00:00] local.DEBUG: Daily log from 2024-01-15'); + File::put($logDir.'/laravel-backup.log', '[2024-01-20 10:00:00] local.DEBUG: Backup log'); + File::put($logDir.'/laravel-error.log', '[2024-01-20 10:00:00] local.DEBUG: Error log'); + File::put($logDir.'/laravel-zzz.log', '[2024-01-20 10:00:00] local.DEBUG: Zzz log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Daily log from 2024-01-15') + ->toolTextDoesNotContain('Backup log') + ->toolTextDoesNotContain('Error log') + ->toolTextDoesNotContain('Zzz log'); +}); From a249567592708ef458ce3df3de1f49cbadf7b653 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 17 Dec 2025 18:48:28 +0530 Subject: [PATCH 3/3] Optimize log path resolution and simplify daily log file handling code Signed-off-by: Pushpak Chhajed --- src/Concerns/ReadsLogs.php | 39 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index efc3082d..cf3dee42 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -4,6 +4,7 @@ namespace Laravel\Boost\Concerns; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; trait ReadsLogs @@ -45,19 +46,18 @@ protected function resolveLogFilePath(): string $channel = Config::get('logging.default'); $channelConfig = Config::get("logging.channels.{$channel}"); - // Handle stack driver by resolving to its first channel with a path $channelConfig = $this->resolveChannelWithPath($channelConfig); - if (($channelConfig['driver'] ?? null) === 'daily') { - return $this->resolveDailyLogFilePath($channelConfig['path'] ?? storage_path('logs/laravel.log')); + $baseLogPath = Arr::get($channelConfig, 'path', storage_path('logs/laravel.log')); + + if (Arr::get($channelConfig, 'driver') === 'daily') { + return $this->resolveDailyLogFilePath($baseLogPath); } - return $channelConfig['path'] ?? storage_path('logs/laravel.log'); + return $baseLogPath; } - /** - * Resolve a channel config that has a path, handling stack drivers recursively. - * + /*** * @param array|null $channelConfig * @return array|null */ @@ -90,14 +90,8 @@ protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0) return $channelConfig; } - /** - * Resolve the daily log file path, falling back to the most recent if today's doesn't exist. - * - * @param string $basePath The configured path (e.g., storage_path('logs/laravel.log')) - */ protected function resolveDailyLogFilePath(string $basePath): string { - // Daily driver appends date before the extension: laravel.log -> laravel-2025-12-14.log $pathInfo = pathinfo($basePath); $directory = $pathInfo['dirname']; $filename = $pathInfo['filename']; @@ -109,19 +103,16 @@ protected function resolveDailyLogFilePath(string $basePath): string return $todayLogFile; } - // Look for the most recent daily log file with matching base name $pattern = $directory.DIRECTORY_SEPARATOR.$filename.'-*'.$extension; - $files = glob($pattern); + $files = glob($pattern) ?: []; - if ($files !== false && $files !== []) { - // Sort by filename (which includes date) in descending order to get most recent - rsort($files); - - return $files[0]; - } + $datePattern = '/^'.preg_quote($filename, '/').'-\d{4}-\d{2}-\d{2}'.preg_quote($extension, '/').'$/'; + $latestFile = collect($files) + ->filter(fn ($file): int|false => preg_match($datePattern, basename((string) $file))) + ->sortDesc() + ->first(); - // Fall back to today's path even if it doesn't exist (error will be handled by caller) - return $todayLogFile; + return $latestFile ?? $todayLogFile; } /** @@ -202,7 +193,7 @@ protected function scanLogChunkForEntries(string $logFile, int $chunkSize): arra $offset = max($fileSize - $chunkSize, 0); fseek($handle, $offset); - // If we started mid-line, discard the partial line to align to next newline. + // If we started mid-line, discard the partial line to align to the next newline. if ($offset > 0) { fgets($handle); }