diff --git a/README.md b/README.md index 73bfd8c..a9dceaf 100644 --- a/README.md +++ b/README.md @@ -179,3 +179,41 @@ $processor = new DataDogProcessor([ ], ]); ``` + +## Tracking the current log context + +Sometimes you want context to be added automatically to all log entries, for example the current +request id ("current plate") for the whole request scope. + +This can be done with `Stefna\Logger\LogContext` together with the `LogContextProcessor`. The +processor needs to be registered once: + +```php +pushProcessor(new \Stefna\Logger\Processor\LogContextProcessor()); +``` + +Set a value for the current request scope: + +```php +getId()); +``` + +Each key holds a single value. Use `unset()` to remove a key again: + +```php +getId()); +\Stefna\Logger\LogContext::set('requestId', $subRequest->getId()); +// all log entries in the sub-scope will include the sub request id +\Stefna\Logger\LogContext::unset('requestId'); +// the request id is no longer tracked +``` + +Values tracked with `set()` are merged into the `context` of every log record processed by the +`LogContextProcessor`. Keys set explicitly on a log call will override the tracked value for that +call. Call `LogContext::clear()` at the end of the request or job to reset all state. diff --git a/src/LogContext.php b/src/LogContext.php new file mode 100644 index 0000000..9930927 --- /dev/null +++ b/src/LogContext.php @@ -0,0 +1,43 @@ + */ + private static array $context = []; + + public static function set(string $key, mixed $value): void + { + self::$context[$key] = $value; + } + + public static function unset(string $key): void + { + unset(self::$context[$key]); + } + + public static function has(string $key): bool + { + return array_key_exists($key, self::$context); + } + + /** + * @return array + */ + public static function getAll(): array + { + return self::$context; + } + + public static function clear(): void + { + self::$context = []; + } +} diff --git a/src/Processor/LogContextProcessor.php b/src/Processor/LogContextProcessor.php new file mode 100644 index 0000000..38ec6ca --- /dev/null +++ b/src/Processor/LogContextProcessor.php @@ -0,0 +1,26 @@ +with( + context: array_merge($context, $record->context), + ); + } +} diff --git a/tests/LogContextTest.php b/tests/LogContextTest.php new file mode 100644 index 0000000..f0c892b --- /dev/null +++ b/tests/LogContextTest.php @@ -0,0 +1,96 @@ +assertTrue(LogContext::has('plate')); + $this->assertSame(['plate' => 'beef'], LogContext::getAll()); + } + + public function testSetReplacesPreviousValue(): void + { + LogContext::set('plate', 'beef'); + LogContext::set('plate', 'pork'); + + $this->assertSame(['plate' => 'pork'], LogContext::getAll()); + } + + public function testHas(): void + { + $this->assertFalse(LogContext::has('plate')); + + LogContext::set('plate', 'beef'); + + $this->assertTrue(LogContext::has('plate')); + } + + public function testUnsetRemovesKey(): void + { + LogContext::set('plate', 'beef'); + + LogContext::unset('plate'); + + $this->assertFalse(LogContext::has('plate')); + $this->assertSame([], LogContext::getAll()); + } + + public function testUnsetUnknownKeyIsNoop(): void + { + LogContext::set('plate', 'beef'); + + LogContext::unset('unknown'); + + $this->assertSame(['plate' => 'beef'], LogContext::getAll()); + } + + public function testUnsetOnlyAffectsSingleKey(): void + { + LogContext::set('plate', 'beef'); + LogContext::set('scope', 'import'); + + LogContext::unset('plate'); + + $this->assertFalse(LogContext::has('plate')); + $this->assertSame(['scope' => 'import'], LogContext::getAll()); + } + + public function testClearResetsEverything(): void + { + LogContext::set('plate', 'beef'); + LogContext::set('scope', 'import'); + + LogContext::clear(); + + $this->assertSame([], LogContext::getAll()); + $this->assertFalse(LogContext::has('plate')); + $this->assertFalse(LogContext::has('scope')); + } + + public function testGetAllReturnsAllValues(): void + { + LogContext::set('plate', 'beef'); + LogContext::set('scope', 'import'); + + $this->assertSame([ + 'plate' => 'beef', + 'scope' => 'import', + ], LogContext::getAll()); + } +} diff --git a/tests/Processor/LogContextProcessorTest.php b/tests/Processor/LogContextProcessorTest.php new file mode 100644 index 0000000..ba24de2 --- /dev/null +++ b/tests/Processor/LogContextProcessorTest.php @@ -0,0 +1,83 @@ +createLogRecord()); + + $this->assertSame([ + 'plate' => 'beef', + ], $newRecord->context); + } + + public function testSetValueAppearsInRecordContext(): void + { + LogContext::set('scope', 'import'); + $processor = new LogContextProcessor(); + + $newRecord = $processor($this->createLogRecord()); + + $this->assertSame([ + 'scope' => 'import', + ], $newRecord->context); + } + + public function testExplicitRecordContextOverridesTrackedValue(): void + { + LogContext::set('plate', 'beef'); + $processor = new LogContextProcessor(); + + $newRecord = $processor($this->createLogRecord([ + 'plate' => 'pork', + ])); + + $this->assertSame([ + 'plate' => 'pork', + ], $newRecord->context); + } + + public function testReturnsSameRecordWhenNoContextIsSet(): void + { + $processor = new LogContextProcessor(); + + $record = $this->createLogRecord(); + + $this->assertSame($record, $processor($record)); + } + + /** + * @param array $context + */ + private function createLogRecord(array $context = []): LogRecord + { + return new LogRecord( + new \DateTimeImmutable(), + 'test', + Level::Info, + 'test', + $context, + ); + } +} +