Browse Source

Added entryTransform option to StreamHandler, removed entryFormat option

main
Dustin Wilson 3 months ago
parent
commit
3221ef0f5c
  1. 62
      README.md
  2. 12
      lib/Logger/Handler.php
  3. 38
      lib/Logger/StreamHandler.php
  4. 29
      tests/cases/TestHandler.php
  5. 91
      tests/cases/TestStreamHandler.php

62
README.md

@ -9,6 +9,7 @@
[i]: https://github.com/symfony/polyfill/tree/main/src/Ctype
[j]: https://github.com/symfony/polyfill/tree/main/src/Mbstring
[k]: https://github.com/php-fig/log
[l]: https://ndjson.org
# Logger #
@ -325,19 +326,60 @@ class StreamHandler extends Handler {
#### Options ####
_entryFormat_: The format of the outputted log entry. Defaults to _"%time% %channel% %level\_name% %message%"_
_entryTransform_: A callable where the log entry can be manipulated. Defaults to _null_
##### Entry Format #####
##### Entry Transform #####
The following are recognized in the _entryFormat_ option:
```php
function (string $time, int $level, string $levelName, string $channel, string $message, array $context): string;
```
###### Parameters ######
<dl>
<dt>time</dt>
<dd>The timestamp the log entry was dispatched at; can be altered with the <i>timeFormat</i> option</dd>
<dt>level</dt>
<dd>The RFC 5424 level integer for the entry</dd>
<dt>levelName</dt>
<dd>The RFC 5424 level name for the entry</dd>
<dt>channel</dt>
<dd>The channel defined when creating the logger</dd>
<dt>message</dt>
<dd>The message string; can be manipulated with the <i>messageTransform</i> option</dd>
<dt>context</dt>
<dd>The context array used when dispatching the entry</dd>
</dl>
| Format | Description | Example |
| ------------ | ----------------- | ---------------------------------------------------------------------- |
| %channel% | channel name | ook |
| %level% | log level integer | 1 |
| %level_name% | log level name | ALERT |
| %message% | log message | Uncaught Error: Call to undefined function ook() in /path/to/ook.php:7 |
| %time% | timestamp | Apr 08 09:58:12 |
Here is an example of how to use the _entryTransform_ option to output entries to `php://stdout` as [NDJSON][l]:
```php
use MensBeam\Logger,
MensBeam\Logger\StreamHandler;
$handler = new StreamHandler(options: [
'entryTransform' => function (string $time, int $level, string $levelName, string $channel, string $message, array $context): string {
$entry = [
'time' => $time,
'level' => $level,
'levelName' => $levelName,
'channel' => $channel,
'message' => $message
];
return json_encode($entry, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
}
]);
$logger = new Logger('ook', $handler);
$logger->info("ook\neek");
// {"time":"Feb 03 22:45:17","level":6,"levelName":"Info","channel":"ook","message":"ook\neek"}
```
#### MensBeam\Logger\StreamHandler::getStream ####

12
lib/Logger/Handler.php

@ -54,7 +54,9 @@ abstract class Handler {
}
if ($name === 'messageTransform' && !is_callable($value)) {
throw new TypeError(sprintf('Value of messageTransform option must be callable'));
$type = gettype($value);
$type = ($type === 'object') ? $value::class : $type;
throw new TypeError(sprintf('Value of messageTransform option must be callable, %s given', $type));
}
$name = "_$name";
@ -70,8 +72,12 @@ abstract class Handler {
$message = trim($message);
if ($this->_messageTransform !== null) {
$t = $this->_messageTransform;
$message = $t($message, $context);
$message = call_user_func($this->_messageTransform, $message, $context);
if (!is_string($message)) {
$type = gettype($message);
$type = ($type === 'object') ? $message::class : $type;
throw new TypeError(sprintf('Return value of messageTransform option callable must be a string, %s given', $type));
}
}
$this->invokeCallback($time, $level, $channel ?? '', $message, $context);

38
lib/Logger/StreamHandler.php

@ -19,7 +19,7 @@ class StreamHandler extends Handler {
protected ?string $uri = null;
protected ?string $uriScheme = null;
protected ?string $_entryFormat = '%time% %channel% %level_name% %message%';
protected $_entryTransform = null;
public function __construct($stream = 'php://stdout', array $levels = [ 0, 1, 2, 3, 4, 5, 6, 7 ], array $options = []) {
@ -41,12 +41,6 @@ class StreamHandler extends Handler {
}
$this->setStream($stream);
// Bad dog, no biscuit!
if (($options['entryFormat'] ?? null) === '') {
$options['entryFormat'] = $this->_entryFormat;
}
parent::__construct($levels, $options);
}
@ -61,6 +55,16 @@ class StreamHandler extends Handler {
return $this->uri;
}
public function setOption(string $name, mixed $value): void {
if ($name === 'entryTransform' && !is_callable($value)) {
$type = gettype($value);
$type = ($type === 'object') ? $value::class : $type;
throw new TypeError(sprintf('Value of entryTransform option must be callable, %s given', $type));
}
parent::setOption($name, $value);
}
public function setStream($value): void {
$isResource = is_resource($value);
if (!$isResource && !is_string($value)) {
@ -90,17 +94,17 @@ class StreamHandler extends Handler {
protected function invokeCallback(string $time, int $level, string $channel, string $message, array $context = []): void {
// Do entry formatting here.
$output = trim(preg_replace_callback('/%([a-z_]+)%/', function($m) use ($time, $level, $channel, $message) {
switch ($m[1]) {
case 'channel': return $channel;
case 'level': return (string)$level;
case 'level_name': return strtoupper(Level::from($level)->name);
case 'message': return $message;
case 'time': return $time;
default: return '';
if ($this->_entryTransform !== null) {
$output = call_user_func($this->_entryTransform, $time, $level, Level::from($level)->name, $channel, $message, $context);
if (!is_string($output)) {
$type = gettype($output);
$type = ($type === 'object') ? $output::class : $type;
throw new TypeError(sprintf('Return value of entryTransform option callable must be a string, %s given', $type));
}
}, $this->_entryFormat));
} else {
$output = sprintf('%s %s %s %s', $time, $channel, strtoupper(Level::from($level)->name), $message);
}
// If output contains any newlines then add an additional newline to aid readability.
if (str_contains($output, \PHP_EOL)) {
$output .= \PHP_EOL;

29
tests/cases/TestHandler.php

@ -98,16 +98,43 @@ class TestHandler extends ErrorHandlingTestCase {
}
// 'Return value of messageTransform option callable must be a string, %s given'
public static function provideFatalErrorTests(): iterable {
$iterable = [
[
TypeError::class,
0,
'Value of messageTransform option must be callable',
'Value of messageTransform option must be callable, integer given',
function (Handler $h): void {
$h->setOption('messageTransform', 42);
}
],
[
TypeError::class,
0,
'Value of messageTransform option must be callable, DateTimeImmutable given',
function (Handler $h): void {
$h->setOption('messageTransform', new \DateTimeImmutable());
}
],
[
TypeError::class,
0,
'Return value of messageTransform option callable must be a string, integer given',
function (Handler $h): void {
$h->setOption('messageTransform', fn() => 42);
$h(Level::Error->value, 'fail', 'fail');
}
],
[
TypeError::class,
0,
'Return value of messageTransform option callable must be a string, DateTimeImmutable given',
function (Handler $h): void {
$h->setOption('messageTransform', fn() => new \DateTimeImmutable());
$h(Level::Error->value, 'fail', 'fail');
}
],
[
InvalidArgumentException::class,
0,

91
tests/cases/TestStreamHandler.php

@ -7,19 +7,24 @@
declare(strict_types=1);
namespace MensBeam\Logger\Test;
use MensBeam\Logger,
org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
use MensBeam\Logger\{
InvalidArgumentException,
IOException,
Level,
StreamHandler
StreamHandler,
TypeError
};
use PHPUnit\Framework\{
TestCase,
Attributes\CoversClass,
Attributes\DataProvider
};
#[CoversClass('MensBeam\Logger\StreamHandler')]
class TestStreamHandler extends TestCase {
/** @covers \MensBeam\Logger\StreamHandler */
class TestStreamHandler extends \PHPUnit\Framework\TestCase {
/** @dataProvider provideResourceTypesTests */
#[DataProvider('provideResourceTypesTests')]
public function testResourceTypes(\Closure $closure): void {
$regex = '/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ERROR Ook!\nEek!\n/';
$this->assertEquals(1, preg_match($regex, $closure()));
@ -39,20 +44,20 @@ class TestStreamHandler extends \PHPUnit\Framework\TestCase {
$this->assertSame(CWD . '/ook', $h->getURI());
}
/** @dataProvider provideEntryFormattingTests */
public function testEntryFormatting(string $entryFormat, string $regex): void {
#[DataProvider('provideEntryTransformingTests')]
public function testEntryTransforming(\Closure $entryTransform, string $regex): void {
$s = fopen('php://memory', 'r+');
$h = new StreamHandler(stream: $s, options: [
'entryFormat' => $entryFormat
'entryTransform' => $entryTransform
]);
$h(Level::Error->value, 'ook', 'ook');
rewind($s);
$o = stream_get_contents($s);
$this->assertEquals(1, preg_match($regex, $o));
$this->assertMatchesRegularExpression($regex, $o);
fclose($s);
}
/** @dataProvider provideFatalErrorTests */
#[DataProvider('provideFatalErrorTests')]
public function testFatalErrors(string $throwableClassName, int $code, string $message, \Closure $closure): void {
$this->expectException($throwableClassName);
$this->expectExceptionMessage($message);
@ -60,7 +65,8 @@ class TestStreamHandler extends \PHPUnit\Framework\TestCase {
$this->expectExceptionCode($code);
}
$closure(new StreamHandler());
$s = fopen('php://memory', 'r+');
$closure(new StreamHandler($s));
}
@ -97,11 +103,20 @@ class TestStreamHandler extends \PHPUnit\Framework\TestCase {
}
}
public static function provideEntryFormattingTests(): iterable {
public static function provideEntryTransformingTests(): iterable {
$iterable = [
[ '%ook%', '/\n/' ],
[ '%channel% %channel% %channel% %channel% %level% %level_name%', '/ook ook ook ook 3 ERROR\n/' ],
[ '', '/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ERROR ook\n/' ]
[
function (): string {
return '';
},
'/\n/'
],
[
function (string $time, int $level, string $levelName, string $channel): string {
return "$channel $channel $channel $channel $level $levelName";
},
'/ook ook ook ook 3 Error\n/'
]
];
foreach ($iterable as $i) {
@ -118,6 +133,48 @@ class TestStreamHandler extends \PHPUnit\Framework\TestCase {
function (StreamHandler $h): void {
new StreamHandler(42);
}
],
[
InvalidArgumentException::class,
0,
'Argument #1 ($value) must be of type resource|string, DateTimeImmutable given',
function (StreamHandler $h): void {
new StreamHandler(new \DateTimeImmutable());
}
],
[
TypeError::class,
0,
'Value of entryTransform option must be callable, integer given',
function (StreamHandler $h): void {
$h->setOption('entryTransform', 42);
}
],
[
TypeError::class,
0,
'Value of entryTransform option must be callable, DateTimeImmutable given',
function (StreamHandler $h): void {
$h->setOption('entryTransform', new \DateTimeImmutable());
}
],
[
TypeError::class,
0,
'Return value of entryTransform option callable must be a string, integer given',
function (StreamHandler $h): void {
$h->setOption('entryTransform', fn() => 42);
$h(Level::Error->value, 'fail', 'fail');
}
],
[
TypeError::class,
0,
'Return value of entryTransform option callable must be a string, DateTimeImmutable given',
function (StreamHandler $h): void {
$h->setOption('entryTransform', fn() => new \DateTimeImmutable());
$h(Level::Error->value, 'fail', 'fail');
}
]
];

Loading…
Cancel
Save