Commit b27be275 authored by Tim Lieberman's avatar Tim Lieberman
Browse files

Added `addContext(array $context)`; rename `child()` to `withContext()`:

In some contexts, it just makes more sense to layer context onto an existing
Logger. The impetus for this change was a desire to decorate the context of a
Logger in some mezzio HTTP middleware. I wanted to capture some data from the
request, and attach it to the Logger. The middleware receives the logger via
constructor-injection from the DIC, and does not pass it on. The middleware
can't know about the DIC, so this seems to be the best solution, even if it
makes makes things feel less immutable.
parent d2dcb4c4
......@@ -26,6 +26,7 @@ processors or handlers added, without adding those to the parent.
## Usage
### Child Loggers
```php
use TimDev\StackLogger\StackLoggerTrait;
......@@ -44,7 +45,7 @@ $mainLogger->info("Have some Context", ['my' => 'context']);
// => [2020-10-17 17:40:53] app.INFO: Have some Context {"my": "context"}
// but you might want to accumulate some context
$child1 = $mainLogger->child(['child' => 'context']);
$child1 = $mainLogger->withContext(['child' => 'context']);
$child1->info('From a child.', ['call-time' => 'context']);
// => [2020-10-17 17:40:53] app.INFO: From a child. {"child":"context","call-time":"context"}
......
......@@ -5,9 +5,26 @@ namespace TimDev\StackLogger;
interface LoggerInterface extends \Psr\Log\LoggerInterface
{
/**
* Returns a new instance that has $context merged on top of any existing context.
*
* Returns a new instance that has $context merged on top of any existing
* context.
*
* Useful when you're passing a logger up the call stack by passing or
* receiving the logger up or down the call stack.
*
* @return static
*/
public function child(array $context = []);
public function withContext(array $context = []);
/**
* Merge $context onto this instance's context.
*
* Useful in contexts like HTTP middleware, where you want to add
* some global context onto the logger, but aren't passing the a
* logger onwards as an argument.
*
* @param array $context
* @return static
*/
public function addContext(array $context);
}
......@@ -4,18 +4,20 @@ declare(strict_types=1);
namespace TimDev\StackLogger;
/**
* If you're extending a Monolog\Logger, and your code calls Monolog\Logger::addRecord() directly, you can use this
* trait instead of StackLoggerTrait.
* If you're extending a Monolog\Logger, and your code calls
* Monolog\Logger::addRecord() directly, you can use this trait instead of
* StackLoggerTrait.
*/
trait MonologStackLoggerTrait
trait MonologStackLoggerTrait
{
use StackLoggerTrait;
/**
* {@inheritDoc}
*
* Monolog predates PSR-3, and exposes a pubic `addRecord()` method, which contains the foundational logic, and upon
* which its implementation of log()/debug()/info()/... depend. So we override it here to preserve our
* Monolog predates PSR-3, and exposes a pubic `addRecord()` method, which
* contains the foundational logic, and upon which its implementation of
* log()/debug()/info()/... depend. So we override it here to preserve our
* context-accumulation and callable-handling features.
*/
public function addRecord(int $level, string $message, array $context = []): bool
......@@ -28,15 +30,18 @@ trait MonologStackLoggerTrait
* {@inheritDoc}
*
* This method from Monolog is special because it returns a clone.
*
* Note: the return type hint must be \Monolog\Logger to stay compatible with the parent method's signature. Oddly,
* if we were not using a trait here, we could specify the return type as `self`, but PHP won't allow us to do that
* from a trait for some reason.
*
* To deal with it, we explicitly (and imprecisely) hint \Monolog\Logger, and add a phpDoc directives specifying
* `static`. This manages to satisfy both the PHP runtime and my (Jetbrains) IDE. In a PHP8 future, once Monolog
* updates the return type of withName() to be static, we can use static here as well.
*
*
* Note: the return type hint must be \Monolog\Logger to stay compatible
* with the parent method's signature. Oddly, if we were not using a trait
* here, we could specify the return type as `self`, but PHP won't allow us
* to do that from a trait for some reason.
*
* To deal with it, we explicitly (and imprecisely) hint \Monolog\Logger,
* and add a phpDoc directives specifying `static`. This manages to satisfy
* both the PHP runtime and my (Jetbrains) IDE. In a PHP8 future, once
* Monolog updates the return type of withName() to be static, we can use
* static here as well.
*
* @return static
*/
public function withName(string $name): \Monolog\Logger
......@@ -44,5 +49,5 @@ trait MonologStackLoggerTrait
$new = parent::withName($name);
$new->context = $this->context;
return $new;
}
}
}
......@@ -10,16 +10,26 @@ trait StackLoggerTrait
/**
* {@inheritDoc}
*/
public function child(array $context = []): self
public function withContext(array $context = []): self
{
$child = clone $this;
$child->context = array_merge($this->context, $context);
$child->addContext($context);
return $child;
}
/**
* Many (but crucially, not all, you need to check!) PSR3 loggers use `log()` as the fundamental method for logging
* messages. In those cases, we can simply intercept log() calls and do our magic.
* {@inheritDoc}
*/
public function addContext(array $context): self
{
$this->context = array_merge($this->context, $context);
return $this;
}
/**
* Many (but crucially, not all, you need to check!) PSR3 loggers use
* `log()` as the fundamental method for logging messages. In those cases,
* we can simply intercept log() calls and do our magic.
*/
public function log($level, $message, array $context = []): void
{
......@@ -32,8 +42,9 @@ trait StackLoggerTrait
}
/**
* Merges $context on top of the instances accumulated context, and processes any callable elements in the final
* context. Factored out from log() above, since it's occasionally usefule elsewhere (like in
* Merges $context on top of the instances accumulated context, and
* processes any callable elements in the final context. Factored out from
* log() above, since it's occasionally usefule elsewhere (like in
* `MonologStackLoggerTrait::addRecord()`.
*/
protected function processContext(array $context): array
......
......@@ -11,14 +11,16 @@ use TimDev\StackLogger\Test\Support\TestLoggerInterface;
/**
* Base class for testing against various loggers.
*
* We want to run these tests against extensions of various logger implementations. Actual test classes extend this
* and implement makeTestSubject().
* We want to run these tests against extensions of various logger
* implementations. Actual test classes extend this and implement
* makeTestSubject().
*/
abstract class BaseTest extends TestCase
{
/**
* This is private because property types are invariant (since they're r/w). Concrete subclasses can rely on
* makeTextSubject() directly in each test, or define their own typed private $log.
* This is private because property types are invariant (since they're r/w).
* Concrete subclasses can rely on makeTextSubject() directly in each test,
* or define their own typed private $log.
*/
private TestLoggerInterface $log;
......@@ -39,17 +41,36 @@ abstract class BaseTest extends TestCase
$this->assertEquals('Log me some warning.', $records[1]['message']);
}
public function testAddsContext()
{
$this->log->addContext(['some' => 'context']);
$this->log->info('An info.');
$this->assertEquals(['some'], $this->log->contextKeysAt(0));
$this->log->addContext(['more' => 'context']);
$this->log->info('I should have two bits of context.');
$this->assertEquals(2, $this->log->contextCountAt(1));
$this->assertEquals(['some', 'more'], $this->log->contextKeysAt(1));
$this->log->addContext(['even more' => 'context']);
$this->log->warning('This message should get four context elements.', ['foo' => 'bar']);
$this->assertEquals(4, $this->log->contextCountAt(2));
$this->log->debug('Back to three!');
$this->assertEquals(['some', 'more', 'even more'], $this->log->contextKeysAt(3));
}
public function testCreateChildWithContext()
{
$log = $this->log->child(['initial' => 'context']);
$log = $this->log->withContext(['initial' => 'context']);
$log->debug('I should have some context from my constructor arg');
$this->assertEquals('context', $this->log->recordAt(0)['context']['initial']);
}
public function testAccumulatesContext()
{
$log = $this->log->child(['initial' => 'context']);
$child = $log->child(['more' => 'context']);
$log = $this->log->withContext(['initial' => 'context']);
$child = $log->withContext(['more' => 'context']);
// $child should have two context items.
$child->warning('I should have three context items', ['final' => 'context']);
......@@ -70,11 +91,11 @@ abstract class BaseTest extends TestCase
public function textMergesContext()
{
$log = $this->log->child(['a' => 'Alice']);
$log = $this->log->withContext(['a' => 'Alice']);
// Alice should be overwritten with Allison in child.
$log
->child(['a' => 'Allison', 'b' => 'Bob'])
->withContext(['a' => 'Allison', 'b' => 'Bob'])
->info('Allison and Bruno', ['b' => 'Bruno']);
$this->assertEquals(
['Allison', 'Bruno'],
......@@ -92,7 +113,7 @@ abstract class BaseTest extends TestCase
public function testInvokesCallables()
{
$logger = $this->log;
$child = $logger->child(
$child = $logger->withContext(
[
// A callable context element that returns the number of
// elements in the record's context.
......@@ -104,15 +125,15 @@ abstract class BaseTest extends TestCase
$child->notice('Only one context element, the result of the callable.');
$this->assertEquals(1, $this->log->contextAt(0)['counter']);
$child = $child->child(['Second' => 'Context Item']);
$child = $child->withContext(['Second' => 'Context Item']);
$child->warning('A log with three context items.', ['Third' => 'Context Item']);
$this->assertEquals(
$this->log->contextCountAt(1), // === 3
$this->log->contextCountAt(1), // === 3
$this->log->contextAt(1)['counter']
);
$start = microtime(true);
$child2 = $logger->child(['elapsed_micros' => fn() => 1000000 * (microtime(true) - $start)]);
$child2 = $logger->withContext(['elapsed_micros' => fn() => 1000000 * (microtime(true) - $start)]);
usleep(1000);
$child2->info('At least 1000 μ-sec have passed.');
......
......@@ -6,12 +6,12 @@ declare(strict_types=1);
namespace TimDev\StackLogger\Test;
use TimDev\StackLogger\Test\Support\ExtendedMonologLogger;
use TimDev\StackLogger\Test\Support\TestLoggerInterface;
/**
* Test against monolog.
*
* Uses ExtendedMonologLogger as a subject, with additional test method(s) for monolog-specific stuff.
* Uses ExtendedMonologLogger as a subject, with additional test method(s) for
* monolog-specific stuff.
*/
class MonologTest extends BaseTest
{
......@@ -23,8 +23,8 @@ class MonologTest extends BaseTest
public function test_withName_cloning()
{
// push some context.
$log = $this->makeTestSubject()->child(['basic' => 'context']);
// push some context.
$log = $this->makeTestSubject()->withContext(['basic' => 'context']);
// get a clone with a new monolog channel-name, and log to it.
$newChannel = $log->withName('other');
......@@ -37,9 +37,9 @@ class MonologTest extends BaseTest
$this->assertCount(1, $rec['context']);
$this->assertEquals('context', $rec['context']['basic']);
/*
This is mostly to prove that static analysis knows $newChannel is an ExtendedMonologLogger, even though
`StackMonologLoggerTrait::withName(): \Monolog\Logger` tells us it isn't.
/*
This is mostly to prove that static analysis knows $newChannel is an ExtendedMonologLogger, even though
`StackMonologLoggerTrait::withName(): \Monolog\Logger` tells us it isn't.
*/
$this->assertTrue($newChannel->extraMethod());
}
......
......@@ -8,7 +8,8 @@ use Monolog\Logger;
use TimDev\StackLogger\MonologStackLoggerTrait;
/**
* Here we extend Monolog\Logger by applying the StackMonologLoggerTrait trait, and add an extra method just for laughs.
* Here we extend Monolog\Logger by applying the StackMonologLoggerTrait trait,
* and add an extra method just for laughs.
*/
class ExtendedMonologLogger extends Logger implements TestLoggerInterface
{
......@@ -21,7 +22,8 @@ class ExtendedMonologLogger extends Logger implements TestLoggerInterface
}
/**
* Used in a test to demonstrate static analysis understands phpDoc directives in the trait.
* Used in a test to demonstrate static analysis understands phpDoc
* directives in the trait.
*/
public function extraMethod(): bool
{
......
......@@ -15,17 +15,17 @@ final class ExtendedTestLogger extends TestLogger implements TestLoggerInterface
{
// This is all that's required to extend your PSR-3 Logger.
use StackLoggerTrait;
// Test-Helpers
use TestLoggerTrait;
public $records;
public $recordsByLevel;
/**
* Replace TestLogger's $records arrays with an ArrayObject, which be copied by reference during a clone, in the
* same way as in the real world, where clones get references to the original handlers/resources/etc.
*
* Replace TestLogger's $records arrays with an ArrayObject, which be copied
* by reference during a clone, in the same way as in the real world, where
* clones get references to the original handlers/resources/etc.
*/
public function __construct() {
$this->records = new \ArrayObject();
......
......@@ -5,9 +5,10 @@ namespace TimDev\StackLogger\Test\Support;
use TimDev\StackLogger\LoggerInterface;
/**
* The unit tests expect to test loggers that implement this interface, which composes PSR-3 and some helper methods for
* use in assertions. You can use the TestLoggerTrait to get most of this. You may need to override a method or two, as
* we do in ExtendedMonologLogger
* The unit tests expect to test loggers that implement this interface, which
* composes PSR-3 and some helper methods for use in assertions. You can use the
* TestLoggerTrait to get most of this. You may need to override a method or
* two, as we do in ExtendedMonologLogger
*/
interface TestLoggerInterface extends LoggerInterface
{
......@@ -18,8 +19,8 @@ interface TestLoggerInterface extends LoggerInterface
public function contextValuesAt($recordIndex);
public function contextCountAt($recordIndex);
public function getRecords(): array;
public function recordAt(int $index): ?array;
}
......@@ -5,8 +5,8 @@ namespace TimDev\StackLogger\Test\Support;
/**
* A trait that implements TestLoggerInterface.
*
* If your logger implementation has some way of buffering records as an array, you can probably leverage this,
* overriding getRecords() as necessary.
* If your logger implementation has some way of buffering records as an array,
* you can probably leverage this, overriding getRecords() as necessary.
*/
trait TestLoggerTrait
{
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment