Skip to content

Commit 0ddba73

Browse files
committed
Merge branch '2.x' into main
2 parents 9992c77 + 5579edf commit 0ddba73

File tree

7 files changed

+172
-14
lines changed

7 files changed

+172
-14
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ New deprecations:
4141
value equal to what `Logger::WARNING` was giving you.
4242
- `Logger::getLevelName()` is now deprecated.
4343

44+
### 2.7.0 (2022-06-09)
45+
46+
* Added `$datetime` parameter to `Logger::addRecord` as low level API to allow logging into the past or future (#1682)
47+
* Added `Logger::useLoggingLoopDetection` to allow disabling cyclic logging detection in concurrent frameworks (#1681)
48+
* Fixed handling of fatal errors if callPrevious is disabled in ErrorHandler (#1670)
49+
* Marked the reusable `Monolog\Test\TestCase` class as `@internal` to make sure PHPStorm does not show it above PHPUnit, you may still use it to test your own handlers/etc though (#1677)
50+
* Fixed RotatingFileHandler issue when the date format contained slashes (#1671)
51+
4452
### 2.6.0 (2022-05-10)
4553

4654
* Deprecated `SwiftMailerHandler`, use `SymfonyMailerHandler` instead

src/Monolog/ErrorHandler.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class ErrorHandler
4545

4646
private string|null $reservedMemory = null;
4747

48-
/** @var mixed|null */
49-
private $lastFatalTrace = null;
48+
/** @var ?array{type: int, message: string, file: string, line: int, trace: mixed} */
49+
private array|null $lastFatalData = null;
5050

5151
private const FATAL_ERRORS = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
5252

@@ -213,7 +213,7 @@ private function handleError(int $code, string $message, string $file = '', int
213213
} else {
214214
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
215215
array_shift($trace); // Exclude handleError from trace
216-
$this->lastFatalTrace = $trace;
216+
$this->lastFatalData = ['type' => $code, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $trace];
217217
}
218218

219219
if ($this->previousErrorHandler === true) {
@@ -233,12 +233,17 @@ public function handleFatalError(): void
233233
{
234234
$this->reservedMemory = '';
235235

236-
$lastError = error_get_last();
236+
if (is_array($this->lastFatalData)) {
237+
$lastError = $this->lastFatalData;
238+
} else {
239+
$lastError = error_get_last();
240+
}
237241
if (is_array($lastError) && in_array($lastError['type'], self::FATAL_ERRORS, true)) {
242+
$trace = $lastError['trace'] ?? null;
238243
$this->logger->log(
239244
$this->fatalLevel,
240245
'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
241-
['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace]
246+
['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $trace]
242247
);
243248

244249
if ($this->logger instanceof Logger) {

src/Monolog/Handler/RotatingFileHandler.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,11 @@ protected function getGlobPattern(): string
185185
$fileInfo = pathinfo($this->filename);
186186
$glob = str_replace(
187187
['{filename}', '{date}'],
188-
[$fileInfo['filename'], '[0-9][0-9][0-9][0-9]*'],
188+
[$fileInfo['filename'], str_replace(
189+
['Y', 'y', 'm', 'd'],
190+
['[0-9][0-9][0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]'],
191+
$this->dateFormat)
192+
],
189193
$fileInfo['dirname'] . '/' . $this->filenameFormat
190194
);
191195
if (isset($fileInfo['extension'])) {

src/Monolog/Logger.php

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ class Logger implements LoggerInterface, ResettableInterface
134134
*/
135135
private int $logDepth = 0;
136136

137+
/**
138+
* Whether to detect infinite logging loops
139+
*
140+
* This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this
141+
*/
142+
private bool $detectCycles = true;
143+
137144
/**
138145
* @param string $name The logging channel, a simple descriptive name that is attached to all log records
139146
* @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc.
@@ -269,19 +276,29 @@ public function useMicrosecondTimestamps(bool $micro): self
269276
return $this;
270277
}
271278

279+
public function useLoggingLoopDetection(bool $detectCycles): self
280+
{
281+
$this->detectCycles = $detectCycles;
282+
283+
return $this;
284+
}
285+
272286
/**
273287
* Adds a log record.
274288
*
275-
* @param int $level The logging level
276-
* @param string $message The log message
277-
* @param mixed[] $context The log context
278-
* @return bool Whether the record has been processed
289+
* @param int $level The logging level
290+
* @param string $message The log message
291+
* @param mixed[] $context The log context
292+
* @param DateTimeImmutable $datetime Optional log date to log into the past or future
293+
* @return bool Whether the record has been processed
279294
*
280295
* @phpstan-param value-of<Level::VALUES>|Level $level
281296
*/
282-
public function addRecord(int|Level $level, string $message, array $context = []): bool
297+
public function addRecord(int|Level $level, string $message, array $context = [], DateTimeImmutable $datetime = null): bool
283298
{
284-
$this->logDepth += 1;
299+
if ($this->detectCycles) {
300+
$this->logDepth += 1;
301+
}
285302
if ($this->logDepth === 3) {
286303
$this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.');
287304
return false;
@@ -297,7 +314,7 @@ public function addRecord(int|Level $level, string $message, array $context = []
297314
context: $context,
298315
level: self::toMonologLevel($level),
299316
channel: $this->name,
300-
datetime: new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
317+
datetime: $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
301318
extra: [],
302319
);
303320
$handled = false;
@@ -336,7 +353,9 @@ public function addRecord(int|Level $level, string $message, array $context = []
336353

337354
return $handled;
338355
} finally {
339-
$this->logDepth--;
356+
if ($this->detectCycles) {
357+
$this->logDepth--;
358+
}
340359
}
341360
}
342361

src/Monolog/Test/TestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* Lets you easily generate log records and a dummy formatter for testing purposes
2323
*
2424
* @author Jordi Boggiano <[email protected]>
25+
*
26+
* @internal feel free to reuse this to test your own handlers, this is marked internal to avoid issues with PHPStorm https://github.com/Seldaek/monolog/issues/1677
2527
*/
2628
class TestCase extends \PHPUnit\Framework\TestCase
2729
{

tests/Monolog/Handler/RotatingFileHandlerTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,38 @@ public function tearDown(): void
4646
foreach (glob(__DIR__.'/Fixtures/*.rot') as $file) {
4747
unlink($file);
4848
}
49+
50+
if ('testRotationWithFolderByDate' === $this->getName(false)) {
51+
foreach (glob(__DIR__.'/Fixtures/[0-9]*') as $folder) {
52+
$this->rrmdir($folder);
53+
}
54+
}
55+
4956
restore_error_handler();
5057

5158
unset($this->lastError);
5259
}
5360

61+
private function rrmdir($directory) {
62+
if (! is_dir($directory)) {
63+
throw new InvalidArgumentException("$directory must be a directory");
64+
}
65+
66+
if (substr($directory, strlen($directory) - 1, 1) !== '/') {
67+
$directory .= '/';
68+
}
69+
70+
foreach (glob($directory . '*', GLOB_MARK) as $path) {
71+
if (is_dir($path)) {
72+
$this->rrmdir($path);
73+
} else {
74+
unlink($path);
75+
}
76+
}
77+
78+
return rmdir($directory);
79+
}
80+
5481
private function assertErrorWasTriggered($code, $message)
5582
{
5683
if (empty($this->lastError)) {
@@ -141,6 +168,76 @@ public function rotationTests()
141168
];
142169
}
143170

171+
private function createDeep($file)
172+
{
173+
mkdir(dirname($file), 0777, true);
174+
touch($file);
175+
176+
return $file;
177+
}
178+
179+
/**
180+
* @dataProvider rotationWithFolderByDateTests
181+
*/
182+
public function testRotationWithFolderByDate($createFile, $dateFormat, $timeCallback)
183+
{
184+
$old1 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-1)).'/foo.rot');
185+
$old2 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-2)).'/foo.rot');
186+
$old3 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-3)).'/foo.rot');
187+
$old4 = $this->createDeep(__DIR__.'/Fixtures/'.date($dateFormat, $timeCallback(-4)).'/foo.rot');
188+
189+
$log = __DIR__.'/Fixtures/'.date($dateFormat).'/foo.rot';
190+
191+
if ($createFile) {
192+
$this->createDeep($log);
193+
}
194+
195+
$handler = new RotatingFileHandler(__DIR__.'/Fixtures/foo.rot', 2);
196+
$handler->setFormatter($this->getIdentityFormatter());
197+
$handler->setFilenameFormat('{date}/{filename}', $dateFormat);
198+
$handler->handle($this->getRecord());
199+
200+
$handler->close();
201+
202+
$this->assertTrue(file_exists($log));
203+
$this->assertTrue(file_exists($old1));
204+
$this->assertEquals($createFile, file_exists($old2));
205+
$this->assertEquals($createFile, file_exists($old3));
206+
$this->assertEquals($createFile, file_exists($old4));
207+
$this->assertEquals('test', file_get_contents($log));
208+
}
209+
210+
public function rotationWithFolderByDateTests()
211+
{
212+
$now = time();
213+
$dayCallback = function ($ago) use ($now) {
214+
return $now + 86400 * $ago;
215+
};
216+
$monthCallback = function ($ago) {
217+
return gmmktime(0, 0, 0, (int) (date('n') + $ago), 1, (int) date('Y'));
218+
};
219+
$yearCallback = function ($ago) {
220+
return gmmktime(0, 0, 0, 1, 1, (int) (date('Y') + $ago));
221+
};
222+
223+
return [
224+
'Rotation is triggered when the file of the current day is not present'
225+
=> [true, 'Y/m/d', $dayCallback],
226+
'Rotation is not triggered when the file of the current day is already present'
227+
=> [false, 'Y/m/d', $dayCallback],
228+
229+
'Rotation is triggered when the file of the current month is not present'
230+
=> [true, 'Y/m', $monthCallback],
231+
'Rotation is not triggered when the file of the current month is already present'
232+
=> [false, 'Y/m', $monthCallback],
233+
234+
'Rotation is triggered when the file of the current year is not present'
235+
=> [true, 'Y', $yearCallback],
236+
'Rotation is not triggered when the file of the current year is already present'
237+
=> [false, 'Y', $yearCallback],
238+
];
239+
}
240+
144241
/**
145242
* @dataProvider dateFormatProvider
146243
*/
@@ -205,6 +302,7 @@ public function filenameFormatProvider()
205302
['foobar-{date}', true],
206303
['foo-{date}-bar', true],
207304
['{date}-foobar', true],
305+
['{date}/{filename}', true],
208306
['foobar', false],
209307
];
210308
}

tests/Monolog/LoggerTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,28 @@ public function testReset()
740740
$this->assertNotSame($uid1, $processorUid1->getUid());
741741
$this->assertNotSame($uid2, $processorUid2->getUid());
742742
}
743+
744+
/**
745+
* @covers Logger::addRecord
746+
*/
747+
public function testLogWithDateTime()
748+
{
749+
foreach ([true, false] as $microseconds) {
750+
$logger = new Logger(__METHOD__);
751+
752+
$loggingHandler = new LoggingHandler($logger);
753+
$testHandler = new TestHandler();
754+
755+
$logger->pushHandler($loggingHandler);
756+
$logger->pushHandler($testHandler);
757+
758+
$datetime = (new DateTimeImmutable($microseconds))->modify('2022-03-04 05:06:07');
759+
$logger->addRecord(Level::Debug, 'test', [], $datetime);
760+
761+
list($record) = $testHandler->getRecords();
762+
$this->assertEquals($datetime->format('Y-m-d H:i:s'), $record->datetime->format('Y-m-d H:i:s'));
763+
}
764+
}
743765
}
744766

745767
class LoggingHandler implements HandlerInterface

0 commit comments

Comments
 (0)