A simple yet configurable logger for PHP
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

395 lines
15 KiB

1 year ago
[a]: https://www.php-fig.org/psr/psr-3/
[b]: https://www.php-fig.org/psr/psr-3/#12-message
[c]: https://www.php.net/manual/en/function.sprintf.php
[d]: https://www.php-fig.org/psr/psr-3/#13-context
[e]: https://www.php-fig.org/psr/psr-3/#11-basics
[f]: http://tools.ietf.org/html/rfc5424
[g]: https://code.mensbeam.com/MensBeam/Filesystem
[h]: https://code.mensbeam.com/MensBeam/Catcher
1 year ago
[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
3 months ago
[m]: https://jsonlines.org
1 year ago
# Logger #
_Logger_ is a simple yet configurable logger for PHP. It is an opinionated implementation of [PSR-3 Logger Interface][a]. It uses classes called _handlers_ to handle messages to log. Currently there is only one handler: `StreamHandler` which allows for logging to files or to streams such as `php://stdout` or `php://stderr`. Handlers can be easily written and plugged into Logger.
## Opinionated? ##
This library attempts what we're calling an "opinionated" implementation of PSR-3. This is because while it successfully implements `Psr\Log\LoggerInterface` _Logger_ deviates from the true spirit of the specification in various ways:
1 year ago
1. In [section 1.1][e] PSR-3 states that when calling the `log` method with one of the log level constants (later shown to be in `Psr\Log\LogLevel`) it must have the same result as calling the level-specific methods. The log level constants in `Psr\Log\LogLevel` are strings, but the `$level` parameter of the `log` method in `Psr\Log\LoggerInterface` is typeless. The words of the specification suggest that the `$level` parameter should be a string, but the actual code implementors are to use doesn't specify a type. The same section also references [RFC 5424][f] when mentioning the log level methods, but why use strings when there are standardized integers used to identify severity? Since the interface allows for any type for `$level`, _Logger_ will prefer the actual RFC standard's integers but will accept and convert PSR-3's strings internally to the integers just so it can remain PSR-3 compatible.
2. In [section 1.2][b] of the specification it describes an optional feature, placeholders, and requires the implementor to write code to parse out and replace placeholders using a syntax and a method that's not present anywhere else in the entirety of PHP. _Logger_ won't support this feature because a logging library's place is to log messages and not to interpolate template strings. A separate library or a built-in function such as `sprintf` should be used instead. _Logger_ provides a way to transform messages that can be used to hook in a preferred interpolation method if desired, though.
3. The specification in [section 1.3][d] also specifies that if an `Exception` object is passed in the `$context` parameter it must be within an `exception` key. This makes sense, but curiously there's nary a mention of what to do with an `Error` object. They've existed since PHP 7 and can be thrown just like exceptions. _Logger_ will accept any `Throwable` in the `exception` key, but at present does nothing with it. Theoretically future handlers could be written to take advantage of it for structured data.
1 year ago
4. Also in the first item of [section 1.3][d] it states categorically that implementors must not trigger a warning when errant data is in the `$context` array and treat it with _"as much lenience as possible"_. It then states in the following item that if an exception is present in the context data it *must* be in the `exception` key and that implementors *must* verify the `exception` key. This is contradictory. You can't verify the `exception` key without triggering an error or exception when it's wrong. The user should be notified they've made a mistake; it's bad design otherwise. Our solution to this problem is to remove errant throwables from `$context` and also trigger warnings when they're encountered. However, `Logger->$warnOnInvalidContextThrowables` is provided to make it easy to suppress the warnings if necessary.
## Requirements ##
* PHP >= 8.1
* [mensbeam/filesystem][g] >= 1.0
* ext-ctype or [symfony/polyfill-ctype][i] >= 1.8
* ext-mbstring or [symfony/polyfill-mbstring][j] >= 1.8
* [psr/log][k] ^3.0
### Note ###
This library uses [mensbeam/filesystem][g] which provides polyfills for `ext-ctype` and `ext-mbstring`. If you have these extensions installed the polyfills won't be used. However, they are still installed. If you don't want the polyfills needlessly installed you can add this to your project's `composer.json`:
```json
{
"require": {
"ext-ctype": "*",
"ext-mbstring": "*"
},
"provide": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-mbstring": "*"
}
}
```
## Installation ##
```bash
composer require mensbeam/logger
```
## Usage ##
This library works without any configuration, but it might not be quite how you think it would work by default:
```php
use MensBeam\Logger;
$logger = new Logger();
```
This will create a logger that outputs all debug, info, notice, and warning entries to `STDOUT` while any error, critical, alert, and emergency entries are output to `STDERR`. This seems like it would be a bizarre default since it causes duplicate output to the shell on errors. However, if you accompany it with an error handler like [`Catcher`][h] it suddenly makes sense:
```php
use MensBeam\{
Catcher,
Logger
};
use MensBeam\Catcher\PlainTextHandler;
$catcher = new Catcher(new PlainTextHandler([
'logger' => new Logger('log'),
'silent' => true
]));
```
Now, _Logger_ will take care of the printing. But, _Logger_ can do far more.
## Documentation ##
### MensBeam\Logger ###
```php
namespace MensBeam;
use MensBeam\Logger\{
Handler,
Level
};
class Logger implements Psr\Log\LoggerInterface {
public bool $warnOnInvalidContextThrowables = true;
public function __construct(?string $channel = null, Handler ...$handlers);
public function getChannel(): ?string;
public function getHandlers(): array;
public function popHandler(): Handler;
public function pushHandler(Handler ...$handlers): void;
public function setChannel(?string $value): void;
public function setHandlers(Handler ...$handlers): void;
public function shiftHandler(): Handler;
public function unshiftHandler(Handler ...$handlers): void;
public function emergency(string|\Stringable $message, array $context = []): void;
public function alert(string|\Stringable $message, array $context = []): void;
public function critical(string|\Stringable $message, array $context = []): void;
public function error(string|\Stringable $message, array $context = []): void;
public function warning(string|\Stringable $message, array $context = []): void;
public function notice(string|\Stringable $message, array $context = []): void;
public function info(string|\Stringable $message, array $context = []): void;
public function debug(string|\Stringable $message, array $context = []): void;
public function log(int|string|Level $level, string|\Stringable $message, array $context = []): void;
}
```
#### Properties ####
_warnOnInvalidContextThrowables_: When set to true Logger will trigger warnings when invalid `Throwable`s are in the `$context` array in the logging methods.
#### MensBeam\Logger::getChannel ####
Returns the channel name for the instance of _Logger_.
#### MensBeam\Logger::getHandlers ####
Returns an array of the handlers defined for use in the _Logger_ instance.
#### MensBeam\Logger::popHandler ####
Pops the last handler off the stack and returns it
#### MensBeam\Logger::pushHandler ####
Pushes the specified handler(s) onto the stack
#### MensBeam\Logger::setChannel ####
Sets the channel to the specified string
#### MensBeam\Logger::setHandlers ####
Replaces the stack of handlers with those specified as parameters
#### MensBeam\Logger::shiftHandler ####
Shifts the first handler off the stack of handlers and returns it
#### MensBeam\Logger::unshiftHandler ####
Unshifts the specified handler(s) onto the beginning of the stack
#### MensBeam\Logger::emergency ####
Adds an emergency entry to the log
#### MensBeam\Logger::alert ####
Adds an alert entry to the log
#### MensBeam\Logger::critical ####
Adds a critical entry to the log
#### MensBeam\Logger::error ####
Adds an error entry to the log
#### MensBeam\Logger::warning ####
Adds a warning entry to the log
#### MensBeam\Logger::notice ####
Adds a notice entry to the log
#### MensBeam\Logger::info ####
Adds an info entry to the log
#### MensBeam\Logger::debug ####
Adds a debug entry to the log
#### MensBeam\Logger::log ####
Adds an entry to the log in the specified level
### MensBeam\Logger\Level ###
This is an enum of the RFC 5424 integer values for each of the log levels; also provides methods for converting to and from PSR-3 string values.
```php
namespace MensBeam;
enum Level: int {
case Emergency = 0;
case Alert = 1;
case Critical = 2;
case Error = 3;
case Warning = 4;
case Notice = 5;
case Info = 6;
case Debug = 7;
public static function fromPSR3(string $level): self;
public function toPSR3(): string;
}
```
#### MensBeam\Logger\Level::fromPSR3 ####
Takes a provided PSR-3 string level and returns a `Level` enum.
#### MensBeam\Logger\Level::toPSR3 ####
Returns a PSR-3 string level representation of the enum.
1 year ago
### MensBeam\Logger\Handler ###
Handlers inherit from this abstract class. Since it is an abstract class meant for constructing handlers protected methods and properties will be documented here as well.
```php
namespace MensBeam\Logger;
abstract class Handler {
protected array $levels;
protected bool $_bubbles = true;
protected callable $_messageTransform = null;
protected string $_timeFormat = 'M d H:i:s';
public function __construct(array $levels = [ 0, 1, 2, 3, 4, 5, 6, 7 ], array $options = []);
public function getLevels();
public function getOption(string $name): mixed;
public function setLevels(int ...$levels): void;
public function setOption(string $name, mixed $value): void;
public function __invoke(int $level, ?string $channel, string $message, array $context = []): void;
abstract protected function invokeCallback(string $time, int $level, string $channel, string $message, array $context = []): void;
}
```
#### Properties (Protected) ####
_levels_: This is where the levels the handler is configured to support are stored
#### Options ####
1 year ago
Properties which begin with an underscore are all options. They can be set either through the constructor or via `setHandler` by name, removing the underscore (\_) at the beginning. All handlers inherit these options. Options in inherited classes should also begin with an underscore (\_).
_bubbles_: When set to true the stack loop will continue onto the next handler; if false it won't. Defaults to _true_
_messageTransform_: A callable to use to transform messages before outputting. Defaults to _null_
_timeFormat_: The PHP-standard date format which to use for times in output. Defaults to _"M d H:i:s"_
##### Message Transform #####
The _messageTransform_ option allows for manipulation of log messages. It accepts any callable with the following structure:
```php
function (string $message, array $context): string;
```
One common use of this feature would be to do string interpolation which isn't handled by the library. By providing a message transform it's possible to use any preferred method of interpolation:
```php
$handler = new StreamHandler(options: [
'messageTransform' => function (string $message, array $context): string {
return vsprintf($message, $context);
}
]);
```
Of course this is a simplistic example. One would want to convert the `$context` array to numerical keys (or just use numerical keys) before usage in `vsprintf`, but as can be seen it's very possible.
#### MensBeam\Logger\Handler::getLevels ####
Returns the levels the handler is configured to support
#### MensBeam\Logger\Handler::getOption ####
Returns the value of the provided option name
#### MensBeam\Logger\Handler::setLevels ####
Sets the levels the handler will then support
#### MensBeam\Logger\Handler::setOption ####
Sets the provided option with the provided value
#### MensBeam\Logger\Handler::__invoke ####
Outputs/dispatches the log entry
#### MensBeam\Logger\Handler::invokeCallback (protected) ####
1 year ago
A callback method meant to be extended by inherited classes to output/dispatch the log entry
### MensBeam\Logger\StreamHandler ###
```php
namespace MensBeam\Logger;
class StreamHandler extends Handler {
public function __construct(resource|string $stream = 'php://stdout', array $levels = [ 0, 1, 2, 3, 4, 5, 6, 7 ], array $options = []);
public function getStream(): ?resource;
public function getURI(): ?string;
1 year ago
public function setStream(resource|string $value): void;
}
```
#### Options ####
_entryTransform_: A callable where the log entry can be manipulated. Defaults to _null_
1 year ago
##### Entry Transform #####
1 year ago
```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>
1 year ago
3 months ago
Here is an example of how to use the _entryTransform_ option to output entries to `php://stdout` as [JSONLines][m]/[NDJSON][l]/whatever (ugh):
```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"}
```
1 year ago
#### MensBeam\Logger\StreamHandler::getStream ####
Returns the resource where the handler will output to
#### MensBeam\Logger\StreamHandler::getURI ####
Returns the URI where the handler will output to
1 year ago
#### MensBeam\Logger\StreamHandler::setStream ####
Sets where the handler will output to