Browse Source

Added entryTransform option to StreamHandler, removed entryFormat option

main
Dustin Wilson 4 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 [i]: https://github.com/symfony/polyfill/tree/main/src/Ctype
[j]: https://github.com/symfony/polyfill/tree/main/src/Mbstring [j]: https://github.com/symfony/polyfill/tree/main/src/Mbstring
[k]: https://github.com/php-fig/log [k]: https://github.com/php-fig/log
[l]: https://ndjson.org
# Logger # # Logger #
@ -325,19 +326,60 @@ class StreamHandler extends Handler {
#### Options #### #### 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 | Here is an example of how to use the _entryTransform_ option to output entries to `php://stdout` as [NDJSON][l]:
| ------------ | ----------------- | ---------------------------------------------------------------------- |
| %channel% | channel name | ook | ```php
| %level% | log level integer | 1 | use MensBeam\Logger,
| %level_name% | log level name | ALERT | MensBeam\Logger\StreamHandler;
| %message% | log message | Uncaught Error: Call to undefined function ook() in /path/to/ook.php:7 |
| %time% | timestamp | Apr 08 09:58:12 | $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 #### #### MensBeam\Logger\StreamHandler::getStream ####

12
lib/Logger/Handler.php

@ -54,7 +54,9 @@ abstract class Handler {
} }
if ($name === 'messageTransform' && !is_callable($value)) { 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"; $name = "_$name";
@ -70,8 +72,12 @@ abstract class Handler {
$message = trim($message); $message = trim($message);
if ($this->_messageTransform !== null) { if ($this->_messageTransform !== null) {
$t = $this->_messageTransform; $message = call_user_func($this->_messageTransform, $message, $context);
$message = $t($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); $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 $uri = null;
protected ?string $uriScheme = 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 = []) { 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); $this->setStream($stream);
// Bad dog, no biscuit!
if (($options['entryFormat'] ?? null) === '') {
$options['entryFormat'] = $this->_entryFormat;
}
parent::__construct($levels, $options); parent::__construct($levels, $options);
} }
@ -61,6 +55,16 @@ class StreamHandler extends Handler {
return $this->uri; 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 { public function setStream($value): void {
$isResource = is_resource($value); $isResource = is_resource($value);
if (!$isResource && !is_string($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 { protected function invokeCallback(string $time, int $level, string $channel, string $message, array $context = []): void {
// Do entry formatting here. if ($this->_entryTransform !== null) {
$output = trim(preg_replace_callback('/%([a-z_]+)%/', function($m) use ($time, $level, $channel, $message) { $output = call_user_func($this->_entryTransform, $time, $level, Level::from($level)->name, $channel, $message, $context);
switch ($m[1]) { if (!is_string($output)) {
case 'channel': return $channel; $type = gettype($output);
case 'level': return (string)$level; $type = ($type === 'object') ? $output::class : $type;
case 'level_name': return strtoupper(Level::from($level)->name); throw new TypeError(sprintf('Return value of entryTransform option callable must be a string, %s given', $type));
case 'message': return $message;
case 'time': return $time;
default: return '';
} }
}, $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 output contains any newlines then add an additional newline to aid readability.
if (str_contains($output, \PHP_EOL)) { if (str_contains($output, \PHP_EOL)) {
$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 { public static function provideFatalErrorTests(): iterable {
$iterable = [ $iterable = [
[ [
TypeError::class, TypeError::class,
0, 0,
'Value of messageTransform option must be callable', 'Value of messageTransform option must be callable, integer given',
function (Handler $h): void { function (Handler $h): void {
$h->setOption('messageTransform', 42); $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, InvalidArgumentException::class,
0, 0,

91
tests/cases/TestStreamHandler.php

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