From b63ce0bd82bd6d7b05a956aedec0da9ecd09011d Mon Sep 17 00:00:00 2001 From: Martin Seitz Date: Mon, 2 Mar 2026 17:26:24 +0100 Subject: [PATCH 1/5] Use sub-second timestamps in logs Sentry UI shows timestamps as `Feb 18, 17:48:27.000 CET` --- src/Monolog/BreadcrumbHandler.php | 2 +- tests/Monolog/BreadcrumbHandlerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index bb2b60ea06..0682c348bd 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -60,7 +60,7 @@ protected function write($record): void $record['channel'], $record['message'], ($record['context'] ?? []) + ($record['extra'] ?? []), - $record['datetime']->getTimestamp() + (float) $record['datetime']->format('U.u') ); $this->hub->addBreadcrumb($breadcrumb); diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index ffef9e1925..5f370f5500 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -25,7 +25,7 @@ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); - $this->assertEquals($record['datetime']->getTimestamp(), $breadcrumb->getTimestamp()); + $this->assertEquals((float) $record['datetime']->format('U.u'), $breadcrumb->getTimestamp()); $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); From 6a608cbf9960c2821543a10c97ec2b4acbc8820e Mon Sep 17 00:00:00 2001 From: Martin Seitz Date: Mon, 2 Mar 2026 18:41:51 +0100 Subject: [PATCH 2/5] Don't rely on datetime being a Date This fixes the Psalm warning --- src/Monolog/BreadcrumbHandler.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index 0682c348bd..fcb8005b20 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -54,13 +54,18 @@ public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bub */ protected function write($record): void { + $datetime = $record['datetime'] ?? null; + $timestamp = $datetime instanceof \DateTimeInterface + ? (float) ($datetime->format('U.u')) + : null; + $breadcrumb = new Breadcrumb( $this->getBreadcrumbLevel($record['level']), $this->getBreadcrumbType($record['level']), $record['channel'], $record['message'], ($record['context'] ?? []) + ($record['extra'] ?? []), - (float) $record['datetime']->format('U.u') + $timestamp ); $this->hub->addBreadcrumb($breadcrumb); From c7aaa564bff9116c4e072d9e4f5779ea171e7b9b Mon Sep 17 00:00:00 2001 From: Martin Seitz Date: Tue, 3 Mar 2026 17:14:11 +0100 Subject: [PATCH 3/5] Fix pre-1970 timestamps --- src/Monolog/BreadcrumbHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index fcb8005b20..fbda73c26d 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -56,7 +56,7 @@ protected function write($record): void { $datetime = $record['datetime'] ?? null; $timestamp = $datetime instanceof \DateTimeInterface - ? (float) ($datetime->format('U.u')) + ? $datetime->getTimestamp() + (int) $datetime->format('u') / 1000000 : null; $breadcrumb = new Breadcrumb( From cc2f5ceced2ea0effcdd9c2a3b937fe2a0754f1d Mon Sep 17 00:00:00 2001 From: Martin Seitz Date: Thu, 5 Mar 2026 12:26:40 +0100 Subject: [PATCH 4/5] Add a test for timestamps --- tests/Monolog/BreadcrumbHandlerTest.php | 34 +++++++++++++++++++------ tests/Monolog/RecordFactory.php | 10 +++++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index 5f370f5500..cdb7498084 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -21,11 +21,11 @@ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void $hub = $this->createMock(HubInterface::class); $hub->expects($this->once()) ->method('addBreadcrumb') - ->with($this->callback(function (Breadcrumb $breadcrumb) use ($expectedBreadcrumb, $record): bool { + ->with($this->callback(function (Breadcrumb $breadcrumb) use ($expectedBreadcrumb): bool { $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); - $this->assertEquals((float) $record['datetime']->format('U.u'), $breadcrumb->getTimestamp()); + $this->assertEquals($expectedBreadcrumb->getTimestamp(), $breadcrumb->getTimestamp()); $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); @@ -41,12 +41,15 @@ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void */ public static function handleDataProvider(): iterable { + $now = new \DateTimeImmutable(); + $defaultBreadcrumb = new Breadcrumb( Breadcrumb::LEVEL_DEBUG, Breadcrumb::TYPE_DEFAULT, 'channel.foo', 'foo bar', - [] + [], + (float) $now->format('U.u') ); $levelsToBeTested = [ @@ -58,31 +61,46 @@ public static function handleDataProvider(): iterable foreach ($levelsToBeTested as $loggerLevel => $breadcrumbLevel) { yield 'with level ' . Logger::getLevelName($loggerLevel) => [ - RecordFactory::create('foo bar', $loggerLevel, 'channel.foo', [], []), + RecordFactory::create('foo bar', $loggerLevel, 'channel.foo', [], [], $now), $defaultBreadcrumb->withLevel($breadcrumbLevel), ]; } yield 'with level ERROR' => [ - RecordFactory::create('foo bar', Logger::ERROR, 'channel.foo', [], []), + RecordFactory::create('foo bar', Logger::ERROR, 'channel.foo', [], [], $now), $defaultBreadcrumb->withLevel(Breadcrumb::LEVEL_ERROR) ->withType(Breadcrumb::TYPE_ERROR), ]; yield 'with level ALERT' => [ - RecordFactory::create('foo bar', Logger::ALERT, 'channel.foo', [], []), + RecordFactory::create('foo bar', Logger::ALERT, 'channel.foo', [], [], $now), $defaultBreadcrumb->withLevel(Breadcrumb::LEVEL_FATAL) ->withType(Breadcrumb::TYPE_ERROR), ]; yield 'with context' => [ - RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', ['context' => ['foo' => 'bar']], []), + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', ['context' => ['foo' => 'bar']], [], $now), $defaultBreadcrumb->withMetadata('context', ['foo' => 'bar']), ]; yield 'with extra' => [ - RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], ['extra' => ['foo' => 'bar']]), + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], ['extra' => ['foo' => 'bar']], $now), $defaultBreadcrumb->withMetadata('extra', ['foo' => 'bar']), ]; + + yield 'with timestamp' => [ + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], [], new \DateTimeImmutable('1970-01-01 00:00:42.1337 UTC')), + $defaultBreadcrumb->withTimestamp(42.1337), + ]; + + yield 'with zero timestamp' => [ + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], [], new \DateTimeImmutable('1970-01-01 00:00:00.000 UTC')), + $defaultBreadcrumb->withTimestamp(0.0), + ]; + + yield 'with negative timestamp' => [ + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], [], new \DateTimeImmutable('1969-12-31 23:59:56.859 UTC')), + $defaultBreadcrumb->withTimestamp(-3.141), + ]; } } diff --git a/tests/Monolog/RecordFactory.php b/tests/Monolog/RecordFactory.php index be22130590..889bb1f548 100644 --- a/tests/Monolog/RecordFactory.php +++ b/tests/Monolog/RecordFactory.php @@ -19,11 +19,15 @@ final class RecordFactory * * @return array|LogRecord */ - public static function create(string $message, int $level, string $channel, array $context = [], array $extra = []) + public static function create(string $message, int $level, string $channel, array $context = [], array $extra = [], ?\DateTimeImmutable $datetime = null) { + if ($datetime === null) { + $datetime = new \DateTimeImmutable(); + } + if (Logger::API >= 3) { return new LogRecord( - new \DateTimeImmutable(), + $datetime, $channel, Logger::toMonologLevel($level), $message, @@ -39,7 +43,7 @@ public static function create(string $message, int $level, string $channel, arra 'level_name' => Logger::getLevelName($level), 'channel' => $channel, 'extra' => $extra, - 'datetime' => new \DateTimeImmutable(), + 'datetime' => $datetime, ]; } } From cc24b65bc88d5d63d4bdeb2087edbd75e02bd92c Mon Sep 17 00:00:00 2001 From: Martin Seitz Date: Thu, 5 Mar 2026 14:15:24 +0100 Subject: [PATCH 5/5] Fix type annotation --- tests/Monolog/BreadcrumbHandlerTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index cdb7498084..812c5d12fd 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -15,6 +15,8 @@ final class BreadcrumbHandlerTest extends TestCase { /** * @dataProvider handleDataProvider + * + * @param LogRecord|array $record */ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void { @@ -37,7 +39,7 @@ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void } /** - * @return iterable, Breadcrumb}> + * @return iterable, Breadcrumb}> */ public static function handleDataProvider(): iterable {