Skip to content

Commit e4e595a

Browse files
committed
Fix log file resolution for stack logging driver
1 parent b87236e commit e4e595a

File tree

2 files changed

+312
-2
lines changed

2 files changed

+312
-2
lines changed

src/Concerns/ReadsLogs.php

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,83 @@ protected function resolveLogFilePath(): string
4545
$channel = Config::get('logging.default');
4646
$channelConfig = Config::get("logging.channels.{$channel}");
4747

48+
// Handle stack driver by resolving to its first channel with a path
49+
$channelConfig = $this->resolveChannelWithPath($channelConfig);
50+
4851
if (($channelConfig['driver'] ?? null) === 'daily') {
49-
return storage_path('logs/laravel-'.date('Y-m-d').'.log');
52+
return $this->resolveDailyLogFilePath($channelConfig['path'] ?? storage_path('logs/laravel.log'));
53+
}
54+
55+
return $channelConfig['path'] ?? storage_path('logs/laravel.log');
56+
}
57+
58+
/**
59+
* Resolve a channel config that has a path, handling stack drivers recursively.
60+
*
61+
* @param array<string, mixed>|null $channelConfig
62+
* @return array<string, mixed>|null
63+
*/
64+
protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0): ?array
65+
{
66+
if ($channelConfig === null || $depth > 5) {
67+
return $channelConfig;
68+
}
69+
70+
if (($channelConfig['driver'] ?? null) !== 'stack') {
71+
return $channelConfig;
72+
}
73+
74+
$stackChannels = $channelConfig['channels'] ?? [];
75+
76+
foreach ($stackChannels as $stackChannel) {
77+
$stackChannelConfig = Config::get("logging.channels.{$stackChannel}");
78+
79+
if (! is_array($stackChannelConfig)) {
80+
continue;
81+
}
82+
83+
$resolved = $this->resolveChannelWithPath($stackChannelConfig, $depth + 1);
84+
85+
if (isset($resolved['path'])) {
86+
return $resolved;
87+
}
88+
}
89+
90+
return $channelConfig;
91+
}
92+
93+
/**
94+
* Resolve the daily log file path, falling back to the most recent if today's doesn't exist.
95+
*
96+
* @param string $basePath The configured path (e.g., storage_path('logs/laravel.log'))
97+
*/
98+
protected function resolveDailyLogFilePath(string $basePath): string
99+
{
100+
// Daily driver appends date before the extension: laravel.log -> laravel-2025-12-14.log
101+
$pathInfo = pathinfo($basePath);
102+
$directory = $pathInfo['dirname'];
103+
$filename = $pathInfo['filename'];
104+
$extension = isset($pathInfo['extension']) ? '.'.$pathInfo['extension'] : '';
105+
106+
$todayLogFile = $directory.DIRECTORY_SEPARATOR.$filename.'-'.date('Y-m-d').$extension;
107+
108+
if (file_exists($todayLogFile)) {
109+
return $todayLogFile;
110+
}
111+
112+
// Look for the most recent daily log file with matching base name
113+
$pattern = $directory.DIRECTORY_SEPARATOR.$filename.'-*'.$extension;
114+
$files = glob($pattern);
115+
116+
if ($files && count($files) > 0) {
117+
// Sort by filename (which includes date) in descending order to get most recent
118+
rsort($files);
119+
120+
return $files[0];
50121
}
51122

52-
return storage_path('logs/laravel.log');
123+
// Fall back to today's path even if it doesn't exist (error will be handled by caller)
124+
return $todayLogFile;
53125
}
54126

55127
/**
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Support\Facades\Config;
6+
use Illuminate\Support\Facades\File;
7+
use Laravel\Boost\Mcp\Tools\ReadLogEntries;
8+
use Laravel\Mcp\Request;
9+
10+
beforeEach(function (): void {
11+
// Clean up any existing log files before each test
12+
$logDir = storage_path('logs');
13+
$files = glob($logDir.'/*.log');
14+
if ($files) {
15+
foreach ($files as $file) {
16+
File::delete($file);
17+
}
18+
}
19+
});
20+
21+
test('it returns log entries when file exists with single driver', function (): void {
22+
$logFile = storage_path('logs/laravel.log');
23+
24+
Config::set('logging.default', 'single');
25+
Config::set('logging.channels.single', [
26+
'driver' => 'single',
27+
'path' => $logFile,
28+
]);
29+
30+
File::ensureDirectoryExists(dirname($logFile));
31+
32+
$logContent = <<<'LOG'
33+
[2024-01-15 10:00:00] local.DEBUG: First log message
34+
[2024-01-15 10:01:00] local.ERROR: Error occurred
35+
[2024-01-15 10:02:00] local.WARNING: Warning message
36+
LOG;
37+
38+
File::put($logFile, $logContent);
39+
40+
$tool = new ReadLogEntries;
41+
$response = $tool->handle(new Request(['entries' => 2]));
42+
43+
expect($response)->isToolResult()
44+
->toolHasNoError()
45+
->toolTextContains('local.WARNING: Warning message', 'local.ERROR: Error occurred')
46+
->toolTextDoesNotContain('local.DEBUG: First log message');
47+
});
48+
49+
test('it detects daily driver directly and reads configured path', function (): void {
50+
$basePath = storage_path('logs/laravel.log');
51+
$logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log');
52+
53+
Config::set('logging.default', 'daily');
54+
Config::set('logging.channels.daily', [
55+
'driver' => 'daily',
56+
'path' => $basePath,
57+
]);
58+
59+
File::ensureDirectoryExists(dirname($logFile));
60+
61+
$logContent = <<<'LOG'
62+
[2024-01-15 10:00:00] local.DEBUG: Daily log message
63+
LOG;
64+
65+
File::put($logFile, $logContent);
66+
67+
$tool = new ReadLogEntries;
68+
$response = $tool->handle(new Request(['entries' => 1]));
69+
70+
expect($response)->isToolResult()
71+
->toolHasNoError()
72+
->toolTextContains('local.DEBUG: Daily log message');
73+
});
74+
75+
test('it detects daily driver within stack channel', function (): void {
76+
$basePath = storage_path('logs/laravel.log');
77+
$logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log');
78+
79+
Config::set('logging.default', 'stack');
80+
Config::set('logging.channels.stack', [
81+
'driver' => 'stack',
82+
'channels' => ['daily'],
83+
]);
84+
Config::set('logging.channels.daily', [
85+
'driver' => 'daily',
86+
'path' => $basePath,
87+
]);
88+
89+
File::ensureDirectoryExists(dirname($logFile));
90+
91+
$logContent = <<<'LOG'
92+
[2024-01-15 10:00:00] local.DEBUG: Stack with daily log message
93+
LOG;
94+
95+
File::put($logFile, $logContent);
96+
97+
$tool = new ReadLogEntries;
98+
$response = $tool->handle(new Request(['entries' => 1]));
99+
100+
expect($response)->isToolResult()
101+
->toolHasNoError()
102+
->toolTextContains('local.DEBUG: Stack with daily log message');
103+
});
104+
105+
test('it uses custom path from daily channel config', function (): void {
106+
$basePath = storage_path('logs/custom-app.log');
107+
$logFile = storage_path('logs/custom-app-'.date('Y-m-d').'.log');
108+
109+
Config::set('logging.default', 'daily');
110+
Config::set('logging.channels.daily', [
111+
'driver' => 'daily',
112+
'path' => $basePath,
113+
]);
114+
115+
File::ensureDirectoryExists(dirname($logFile));
116+
117+
$logContent = <<<'LOG'
118+
[2024-01-15 10:00:00] local.DEBUG: Custom path log message
119+
LOG;
120+
121+
File::put($logFile, $logContent);
122+
123+
$tool = new ReadLogEntries;
124+
$response = $tool->handle(new Request(['entries' => 1]));
125+
126+
expect($response)->isToolResult()
127+
->toolHasNoError()
128+
->toolTextContains('local.DEBUG: Custom path log message');
129+
});
130+
131+
test('it falls back to most recent daily log when today has no logs', function (): void {
132+
$basePath = storage_path('logs/laravel.log');
133+
134+
Config::set('logging.default', 'daily');
135+
Config::set('logging.channels.daily', [
136+
'driver' => 'daily',
137+
'path' => $basePath,
138+
]);
139+
140+
$logDir = storage_path('logs');
141+
File::ensureDirectoryExists($logDir);
142+
143+
// Create a log file for yesterday
144+
$yesterdayLogFile = $logDir.'/laravel-'.date('Y-m-d', strtotime('-1 day')).'.log';
145+
146+
$logContent = <<<'LOG'
147+
[2024-01-14 10:00:00] local.DEBUG: Yesterday's log message
148+
LOG;
149+
150+
File::put($yesterdayLogFile, $logContent);
151+
152+
$tool = new ReadLogEntries;
153+
$response = $tool->handle(new Request(['entries' => 1]));
154+
155+
expect($response)->isToolResult()
156+
->toolHasNoError()
157+
->toolTextContains('local.DEBUG: Yesterday\'s log message');
158+
});
159+
160+
test('it uses single channel path from stack when no daily channel', function (): void {
161+
$logFile = storage_path('logs/app.log');
162+
163+
Config::set('logging.default', 'stack');
164+
Config::set('logging.channels.stack', [
165+
'driver' => 'stack',
166+
'channels' => ['single'],
167+
]);
168+
Config::set('logging.channels.single', [
169+
'driver' => 'single',
170+
'path' => $logFile,
171+
]);
172+
173+
File::ensureDirectoryExists(dirname($logFile));
174+
175+
$logContent = <<<'LOG'
176+
[2024-01-15 10:00:00] local.DEBUG: Single in stack log message
177+
LOG;
178+
179+
File::put($logFile, $logContent);
180+
181+
$tool = new ReadLogEntries;
182+
$response = $tool->handle(new Request(['entries' => 1]));
183+
184+
expect($response)->isToolResult()
185+
->toolHasNoError()
186+
->toolTextContains('local.DEBUG: Single in stack log message');
187+
});
188+
189+
test('it returns error when entries argument is invalid', function (): void {
190+
$tool = new ReadLogEntries;
191+
192+
// Test with zero
193+
$response = $tool->handle(new Request(['entries' => 0]));
194+
expect($response)->isToolResult()
195+
->toolHasError()
196+
->toolTextContains('The "entries" argument must be greater than 0.');
197+
198+
// Test with negative
199+
$response = $tool->handle(new Request(['entries' => -5]));
200+
expect($response)->isToolResult()
201+
->toolHasError()
202+
->toolTextContains('The "entries" argument must be greater than 0.');
203+
});
204+
205+
test('it returns error when log file does not exist', function (): void {
206+
Config::set('logging.default', 'single');
207+
Config::set('logging.channels.single', [
208+
'driver' => 'single',
209+
'path' => storage_path('logs/laravel.log'),
210+
]);
211+
212+
$tool = new ReadLogEntries;
213+
$response = $tool->handle(new Request(['entries' => 10]));
214+
215+
expect($response)->isToolResult()
216+
->toolHasError()
217+
->toolTextContains('Log file not found');
218+
});
219+
220+
test('it returns error when log file is empty', function (): void {
221+
$logFile = storage_path('logs/laravel.log');
222+
223+
Config::set('logging.default', 'single');
224+
Config::set('logging.channels.single', [
225+
'driver' => 'single',
226+
'path' => $logFile,
227+
]);
228+
229+
File::ensureDirectoryExists(dirname($logFile));
230+
File::put($logFile, '');
231+
232+
$tool = new ReadLogEntries;
233+
$response = $tool->handle(new Request(['entries' => 5]));
234+
235+
expect($response)->isToolResult()
236+
->toolHasNoError()
237+
->toolTextContains('Unable to retrieve log entries, or no entries yet.');
238+
});

0 commit comments

Comments
 (0)