From be6dff5b4ed55dd55fe6d2f606bcec13a11ef39c Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sun, 14 Dec 2025 06:10:18 +0330 Subject: [PATCH] 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.'); +});