Dustin Wilson
1 year ago
commit
fd8ebbea04
15 changed files with 2736 additions and 0 deletions
@ -0,0 +1,76 @@ |
|||||
|
# Project-specific |
||||
|
/test*.* |
||||
|
/test/ |
||||
|
/test/* |
||||
|
|
||||
|
# General |
||||
|
*.DS_Store |
||||
|
.AppleDouble |
||||
|
.LSOverride |
||||
|
|
||||
|
# Icon must end with two \r |
||||
|
Icon |
||||
|
|
||||
|
|
||||
|
# Thumbnails |
||||
|
._* |
||||
|
|
||||
|
# Files that might appear in the root of a volume |
||||
|
.DocumentRevisions-V100 |
||||
|
.fseventsd |
||||
|
.Spotlight-V100 |
||||
|
.TemporaryItems |
||||
|
.Trashes |
||||
|
.VolumeIcon.icns |
||||
|
.com.apple.timemachine.donotpresent |
||||
|
|
||||
|
# Directories potentially created on remote AFP share |
||||
|
.AppleDB |
||||
|
.AppleDesktop |
||||
|
Network Trash Folder |
||||
|
Temporary Items |
||||
|
.apdisk |
||||
|
|
||||
|
# Windows thumbnail cache files |
||||
|
Thumbs.db |
||||
|
ehthumbs.db |
||||
|
ehthumbs_vista.db |
||||
|
|
||||
|
# Dump file |
||||
|
*.stackdump |
||||
|
|
||||
|
# Folder config file |
||||
|
Desktop.ini |
||||
|
|
||||
|
# Recycle Bin used on file shares |
||||
|
$RECYCLE.BIN/ |
||||
|
|
||||
|
# Windows Installer files |
||||
|
*.cab |
||||
|
*.msi |
||||
|
*.msm |
||||
|
*.msp |
||||
|
|
||||
|
# Windows shortcuts |
||||
|
*.lnk |
||||
|
|
||||
|
*~ |
||||
|
|
||||
|
# temporary files which can be created if a process still has a handle open of a deleted file |
||||
|
.fuse_hidden* |
||||
|
|
||||
|
# KDE directory preferences |
||||
|
.directory |
||||
|
|
||||
|
# Linux trash folder which might appear on any partition or disk |
||||
|
.Trash-* |
||||
|
|
||||
|
# .nfs files are created when an open file is removed but is still being accessed |
||||
|
.nfs* |
||||
|
|
||||
|
/vendor/ |
||||
|
/vendor-bin/*/vendor |
||||
|
/tests/html5lib-tests |
||||
|
/tests/.phpunit.*cache |
||||
|
/tests/coverage |
||||
|
cachegrind.out.* |
@ -0,0 +1,20 @@ |
|||||
|
[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 |
||||
|
|
||||
|
# 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. 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. |
@ -0,0 +1,28 @@ |
|||||
|
{ |
||||
|
"name": "mensbeam/logger", |
||||
|
"description": "A simple yet configurable logger", |
||||
|
"type": "library", |
||||
|
"require": { |
||||
|
"php": ">=8.1", |
||||
|
"psr/log": "^3.0", |
||||
|
"mensbeam/filesystem": "^1.0", |
||||
|
"nikic/php-parser": "^4.15" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"autoload": { |
||||
|
"psr-4": { |
||||
|
"MensBeam\\": "lib/" |
||||
|
} |
||||
|
}, |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "Dustin Wilson", |
||||
|
"email": "dustin@dustinwilson.com" |
||||
|
} |
||||
|
], |
||||
|
"require-dev": { |
||||
|
"ext-pcov": "*", |
||||
|
"docopt/docopt": "^1.0", |
||||
|
"phpunit/phpunit": "^10.0" |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,161 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam; |
||||
|
use MensBeam\Logger\{ |
||||
|
Handler, |
||||
|
Level, |
||||
|
StreamHandler |
||||
|
}; |
||||
|
use Psr\Log\{ |
||||
|
InvalidArgumentException, |
||||
|
LoggerInterface |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
class Logger implements LoggerInterface { |
||||
|
public const EMERGENCY = 0; |
||||
|
public const ALERT = 1; |
||||
|
public const CRITICAL = 2; |
||||
|
public const ERROR = 3; |
||||
|
public const WARNING = 4; |
||||
|
public const NOTICE = 5; |
||||
|
public const INFO = 6; |
||||
|
public const DEBUG = 7; |
||||
|
|
||||
|
/** The channel name identifier used for this instance of Logger */ |
||||
|
protected ?string $channel; |
||||
|
|
||||
|
/** |
||||
|
* Array of handlers the exceptions are passed to |
||||
|
* |
||||
|
* @var Handler[] |
||||
|
*/ |
||||
|
protected array $handlers; |
||||
|
|
||||
|
|
||||
|
public function __construct(?string $channel = null, Handler ...$handlers) { |
||||
|
$this->setChannel($channel); |
||||
|
|
||||
|
if (count($handlers) === 0) { |
||||
|
$handlers[] = new StreamHandler('php://stdout'); |
||||
|
} |
||||
|
|
||||
|
$this->handlers = $handlers; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
public function getChannel(): ?string { |
||||
|
return $this->channel; |
||||
|
} |
||||
|
|
||||
|
public function setChannel(?string $value): void { |
||||
|
$this->channel = ($value !== null) ? substr($value, 0, 29) : null; |
||||
|
} |
||||
|
|
||||
|
public function getHandlers(): array { |
||||
|
return $this->handlers; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** System is unusable. */ |
||||
|
public function emergency(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Emergency->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** Action must be taken immediately. */ |
||||
|
public function alert(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Alert->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Critical conditions. |
||||
|
* Example: Application component unavailable, unexpected exception. |
||||
|
*/ |
||||
|
public function critical(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Critical->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Runtime errors that do not require immediate action but should typically |
||||
|
* be logged and monitored. |
||||
|
*/ |
||||
|
public function error(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Error->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** Exceptional occurrences that are not errors. */ |
||||
|
public function warning(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Warning->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** Normal but significant events. */ |
||||
|
public function notice(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Notice->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Interesting events. |
||||
|
* Example: User logs in, SQL logs. |
||||
|
*/ |
||||
|
public function info(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Info->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** Detailed debug information. */ |
||||
|
public function debug(string|\Stringable $message, array $context = []): void { |
||||
|
$this->log(Level::Debug->value, $message, $context); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Logs with an arbitrary level. |
||||
|
* @throws \Psr\Log\InvalidArgumentException |
||||
|
*/ |
||||
|
public function log($level, string|\Stringable $message, array $context = []): void { |
||||
|
// Because the interface won't allow limiting $level to just int|string this is |
||||
|
// necessary. |
||||
|
if (!is_int($level) && !is_string($level)) { |
||||
|
$type = gettype($level); |
||||
|
$type = ($type === 'object') ? $level::class : $type; |
||||
|
throw new \TypeError(sprintf("Expected type 'int|string'. Found '%s'.", $type)); |
||||
|
} |
||||
|
|
||||
|
// If the level is a string convert it to a RFC5424 level integer. |
||||
|
$origLevel = $level; |
||||
|
$level = (is_string($level)) ? Level::fromPSR3($level) : $level; |
||||
|
if ($level < 0 || $level > 7) { |
||||
|
throw new InvalidArgumentException(sprintf('Invalid log level %s', $origLevel)); |
||||
|
} |
||||
|
|
||||
|
# PSR-3: Logger Interface |
||||
|
# §1.3 Context |
||||
|
# |
||||
|
# * Every method accepts an array as context data. This is meant to hold any |
||||
|
# extraneous information that does not fit well in a string. The array can |
||||
|
# contain anything. Implementors MUST ensure they treat context data with as |
||||
|
# much lenience as possible. A given value in the context MUST NOT throw an |
||||
|
# exception nor raise any php error, warning or notice. |
||||
|
# |
||||
|
# * If an Exception object is passed in the context data, it MUST be in the |
||||
|
# 'exception' key. Logging exceptions is a common pattern and this allows |
||||
|
# implementors to extract a stack trace from the exception when the log |
||||
|
# backend supports it. Implementors MUST still verify that the 'exception' key |
||||
|
# is actually an Exception before using it as such, as it MAY contain |
||||
|
# anything. |
||||
|
|
||||
|
// We're not doing interpolation :) |
||||
|
|
||||
|
foreach ($this->handlers as $h) { |
||||
|
$h($level, $this->channel, $message, $context); |
||||
|
if (!$h->getOption('bubbles')) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,101 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger; |
||||
|
|
||||
|
abstract class Handler { |
||||
|
protected array $levels; |
||||
|
protected \DateTimeImmutable $timestamp; |
||||
|
|
||||
|
protected bool $_bubbles = true; |
||||
|
protected string $_datetimeFormat = 'M d H:i:s'; |
||||
|
protected string|\Closure|null $_messageTransform = null; |
||||
|
|
||||
|
|
||||
|
|
||||
|
public function __construct(array $levels = [ 0, 1, 2, 3, 4, 5, 6, 7 ], array $options = []) { |
||||
|
$levelsCount = count($levels); |
||||
|
if ($levelsCount > 8) { |
||||
|
throw new InvalidArgumentException(sprintf('Argument #%s ($levels) cannot have more than 8 values', $this->getParamPosition())); |
||||
|
} |
||||
|
if (count($levels) === 0) { |
||||
|
throw new InvalidArgumentException(sprintf('Argument #%s ($levels) must not be empty', $this->getParamPosition())); |
||||
|
} |
||||
|
|
||||
|
$levels = array_unique($levels, \SORT_NUMERIC); |
||||
|
foreach ($levels as $k => $v) { |
||||
|
if (!is_int($v)) { |
||||
|
$type = gettype($v); |
||||
|
$type = ($type === 'object') ? $v::class : $type; |
||||
|
throw new InvalidArgumentException(sprintf('Value #%s of argument #%s ($levels) must be of type int, %s given', $k, $this->getParamPosition(), $type)); |
||||
|
} |
||||
|
|
||||
|
if ($v < 0 || $v > 7) { |
||||
|
throw new RangeException(sprintf('Argument #%s ($levels) cannot be %s; it is not in the range 0 - 7', $this->getParamPosition(), $v)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$this->levels = array_values($levels); |
||||
|
|
||||
|
foreach ($options as $key => $value) { |
||||
|
$key = "_$key"; |
||||
|
$this->$key = $value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
public function getOption(string $name): mixed { |
||||
|
$class = get_class($this); |
||||
|
if (!property_exists($class, "_$name")) { |
||||
|
trigger_error(sprintf('Undefined option in %s: %s', $class, $name), \E_USER_WARNING); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
$name = "_$name"; |
||||
|
return $this->$name; |
||||
|
} |
||||
|
|
||||
|
public function setOption(string $name, mixed $value): void { |
||||
|
$class = get_class($this); |
||||
|
if (!property_exists($class, "_$name")) { |
||||
|
trigger_error(sprintf('Undefined option in %s: %s', $class, $name), \E_USER_WARNING); |
||||
|
} |
||||
|
|
||||
|
$name = "_$name"; |
||||
|
$this->$name = $value; |
||||
|
} |
||||
|
|
||||
|
public function __invoke(int $level, string $channel, string $message, array $context = []): void { |
||||
|
$datetime = \DateTimeImmutable::createFromFormat('U.u', (string)microtime(true))->format($this->_datetimeFormat); |
||||
|
|
||||
|
$message = trim($message); |
||||
|
if ($this->_messageTransform !== null) { |
||||
|
$t = $this->_messageTransform; |
||||
|
$message = $t($message, $context); |
||||
|
} |
||||
|
|
||||
|
$this->invokeCallback($datetime, $level, $channel, $message, $context); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
abstract protected function invokeCallback(string $datetime, int $level, string $channel, string $message, array $context = []): void; |
||||
|
|
||||
|
|
||||
|
private function getParamPosition(): int { |
||||
|
$params = (new \ReflectionClass(get_called_class()))->getConstructor()->getParameters(); |
||||
|
foreach ($params as $k => $p) { |
||||
|
if ($p->getName() === 'levels') { |
||||
|
return $k + 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return -1; |
||||
|
} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger; |
||||
|
|
||||
|
class IOException extends \RuntimeException { |
||||
|
protected ?string $path; |
||||
|
|
||||
|
public function __construct(string $message, int $code = 0, \Throwable $previous = null, string $path = null) { |
||||
|
$this->path = $path; |
||||
|
|
||||
|
parent::__construct($message, $code, $previous); |
||||
|
} |
||||
|
|
||||
|
public function getPath(): ?string { |
||||
|
return $this->path; |
||||
|
} |
||||
|
} |
@ -0,0 +1,29 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger; |
||||
|
|
||||
|
class InvalidArgumentException extends \InvalidArgumentException { |
||||
|
public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null) { |
||||
|
// Make output a bit more useful by making it show the file and line of where the constructor was called. |
||||
|
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5); |
||||
|
$b = null; |
||||
|
foreach ($backtrace as $k => $v) { |
||||
|
if ($v['function'] === '__construct' && $v['class'] === __NAMESPACE__ . '\Handler') { |
||||
|
$b = $backtrace[$k + 1] ?? null; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if ($b !== null) { |
||||
|
$this->file = $b['file']; |
||||
|
$this->line = $b['line']; |
||||
|
} |
||||
|
|
||||
|
parent::__construct($message, $code, $previous); |
||||
|
} |
||||
|
} |
@ -0,0 +1,73 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger; |
||||
|
use Psr\Log\LogLevel; |
||||
|
|
||||
|
|
||||
|
enum Level: int { |
||||
|
/** System is unusable */ |
||||
|
case Emergency = 0; |
||||
|
|
||||
|
/** Action must be taken immediately */ |
||||
|
case Alert = 1; |
||||
|
|
||||
|
/** |
||||
|
* Critical conditions. |
||||
|
* Example: Application component unavailable, unexpected exception. |
||||
|
*/ |
||||
|
case Critical = 2; |
||||
|
|
||||
|
/** |
||||
|
* Runtime errors that do not require immediate action but should typically |
||||
|
* be logged and monitored. |
||||
|
*/ |
||||
|
case Error = 3; |
||||
|
|
||||
|
/** Exceptional occurrences that are not errors. */ |
||||
|
case Warning = 4; |
||||
|
|
||||
|
/** Normal but significant events. */ |
||||
|
case Notice = 5; |
||||
|
|
||||
|
/** |
||||
|
* Interesting events. |
||||
|
* Example: User logs in, SQL logs. |
||||
|
*/ |
||||
|
case Info = 6; |
||||
|
|
||||
|
/** Detailed debug information. */ |
||||
|
case Debug = 7; |
||||
|
|
||||
|
|
||||
|
public static function fromPSR3(string $level): self { |
||||
|
return match ($level) { |
||||
|
LogLevel::EMERGENCY => self::Emergency, |
||||
|
LogLevel::ALERT => self::Alert, |
||||
|
LogLevel::CRITICAL => self::Critical, |
||||
|
LogLevel::ERROR => self::Error, |
||||
|
LogLevel::WARNING => self::Warning, |
||||
|
LogLevel::NOTICE => self::Notice, |
||||
|
LogLevel::INFO => self::Info, |
||||
|
LogLevel::DEBUG => self::Debug |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public function toPSR3(): string { |
||||
|
return match ($this) { |
||||
|
self::Emergency => LogLevel::EMERGENCY, |
||||
|
self::Alert => LogLevel::ALERT, |
||||
|
self::Critical => LogLevel::CRITICAL, |
||||
|
self::Error => LogLevel::ERROR, |
||||
|
self::Warning => LogLevel::WARNING, |
||||
|
self::Notice => LogLevel::NOTICE, |
||||
|
self::Info => LogLevel::INFO, |
||||
|
self::Debug => LogLevel::DEBUG |
||||
|
}; |
||||
|
} |
||||
|
} |
@ -0,0 +1,29 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger; |
||||
|
|
||||
|
class RangeException extends \RangeException { |
||||
|
public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null) { |
||||
|
// Make output a bit more useful by making it show the file and line of where the constructor was called. |
||||
|
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5); |
||||
|
$b = null; |
||||
|
foreach ($backtrace as $k => $v) { |
||||
|
if ($v['function'] === '__construct' && $v['class'] === __NAMESPACE__ . '\Handler') { |
||||
|
$b = $backtrace[$k + 1] ?? null; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if ($b !== null) { |
||||
|
$this->file = $b['file']; |
||||
|
$this->line = $b['line']; |
||||
|
} |
||||
|
|
||||
|
parent::__construct($message, $code, $previous); |
||||
|
} |
||||
|
} |
@ -0,0 +1,110 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger; |
||||
|
use MensBeam\{ |
||||
|
Filesystem as Fs, |
||||
|
Path |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
class StreamHandler extends Handler { |
||||
|
protected int $chunkSize = 10 * 1024 * 1024; |
||||
|
protected $resource = null; |
||||
|
protected ?string $url = null; |
||||
|
protected ?string $urlScheme = null; |
||||
|
|
||||
|
protected ?string $_entryFormat = '%datetime% %channel% %level_name% %message%'; |
||||
|
|
||||
|
|
||||
|
public function __construct($stream = 'php://stdout', array $levels = [ 0, 1, 2, 3, 4, 5, 6, 7 ], array $options = []) { |
||||
|
// Get the memory limit to determine the chunk size. |
||||
|
|
||||
|
// The memory limit is in a shorthand format |
||||
|
// (https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes), so we |
||||
|
// need it as a integer representation in bytes. |
||||
|
if (preg_match('/^\s*(?<num>\d+)(?:\.\d+)?\s*(?<unit>[gkm])\s*$/i', strtolower(ini_get('memory_limit')), $matches) === 1) { |
||||
|
$num = (int)$matches['num']; |
||||
|
switch ($matches['unit'] ?? '') { |
||||
|
case 'g': $num *= 1024; |
||||
|
case 'm': $num *= 1024; |
||||
|
case 'k': $num *= 1024; |
||||
|
} |
||||
|
|
||||
|
// Use 10% of allowed memory or 100K, whichever is largest |
||||
|
$this->chunkSize = min($this->chunkSize, max((int)($num / 10), 100 * 1024)); |
||||
|
} |
||||
|
|
||||
|
if (is_resource($stream)) { |
||||
|
$this->resource = $stream; |
||||
|
stream_set_chunk_size($this->resource, $this->chunkSize); |
||||
|
} elseif(is_string($stream)) { |
||||
|
$stream = Path::canonicalize($stream); |
||||
|
// This wouldn't be useful for validating a URI schema, but it's fine for what this needs |
||||
|
preg_match('/^(?:(?<scheme>[^:\s\/]+):)?(?<slashes>\/*)/i', $stream, $matches); |
||||
|
if (in_array($matches['scheme'], [ 'file', '' ])) { |
||||
|
$slashCount = strlen($matches['slashes'] ?? ''); |
||||
|
$relative = ($matches['scheme'] === 'file') ? ($slashCount === 0 || $slashCount === 2) : ($slashCount === 0); |
||||
|
$stream = (($relative) ? getcwd() : '') . '/' . substr($stream, strlen($matches[0])); |
||||
|
} |
||||
|
|
||||
|
$this->url = $stream; |
||||
|
$this->urlScheme = $matches['scheme'] ?: 'file'; |
||||
|
} else { |
||||
|
$type = gettype($stream); |
||||
|
$type = ($type === 'object') ? $stream::class : $stream; |
||||
|
throw new \TypeError(sprintf("Expected type 'resource|string'. Found '%s'", $type)); |
||||
|
} |
||||
|
|
||||
|
parent::__construct($levels, $options); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
public function getStream() { |
||||
|
return $this->resource ?? $this->url; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
protected function invokeCallback(string $datetime, int $level, string $channel, string $message, array $context = []): void { |
||||
|
if (!in_array($level, $this->levels)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Do output formatting here. |
||||
|
$output = trim(preg_replace_callback('/%([a-z_]+)%/', function($m) use ($datetime, $level, $channel, $message) { |
||||
|
switch ($m[1]) { |
||||
|
case 'channel': return $channel; |
||||
|
case 'datetime': return $datetime; |
||||
|
case 'level': return (string)$level; |
||||
|
case 'level_name': return strtoupper(Level::from($level)->name); |
||||
|
case 'message': return $message; |
||||
|
default: return ''; |
||||
|
} |
||||
|
}, $this->_entryFormat)); |
||||
|
// If output contains any newlines then add an additional newline to aid readability. |
||||
|
if (str_contains($output, \PHP_EOL)) { |
||||
|
$output .= \PHP_EOL; |
||||
|
} |
||||
|
$output .= \PHP_EOL; |
||||
|
|
||||
|
if ($this->resource === null) { |
||||
|
if ($this->urlScheme === 'file') { |
||||
|
Fs::mkdir(dirname($this->url)); |
||||
|
} |
||||
|
|
||||
|
$fp = fopen($this->url, 'a'); |
||||
|
stream_set_chunk_size($fp, $this->chunkSize); |
||||
|
fwrite($fp, $output); |
||||
|
fclose($fp); |
||||
|
} else { |
||||
|
fwrite($this->resource, $output); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
#!/usr/bin/env php |
||||
|
<?php |
||||
|
$dir = ini_get('extension_dir'); |
||||
|
$php = escapeshellarg(\PHP_BINARY); |
||||
|
$code = escapeshellarg(__DIR__ . '/lib'); |
||||
|
|
||||
|
|
||||
|
array_shift($argv); |
||||
|
foreach ($argv as $k => $v) { |
||||
|
if (in_array($v, ['--coverage', '--coverage-html'])) { |
||||
|
$argv[$k] = '--coverage-html tests/coverage'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$cmd = [ |
||||
|
$php, |
||||
|
'-d opcache.enable_cli=0', |
||||
|
]; |
||||
|
|
||||
|
if (!extension_loaded('pcov')) { |
||||
|
$cmd[] = '-d extension=pcov.so'; |
||||
|
} |
||||
|
|
||||
|
$cmd = implode(' ', [ |
||||
|
...$cmd, |
||||
|
'-d pcov.enabled=1', |
||||
|
"-d pcov.directory=$code", |
||||
|
escapeshellarg(__DIR__ . '/vendor/bin/phpunit'), |
||||
|
'--configuration tests/phpunit.xml', |
||||
|
...$argv |
||||
|
]); |
||||
|
passthru($cmd); |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger\Test; |
||||
|
|
||||
|
ini_set('memory_limit', '-1'); |
||||
|
ini_set('zend.assertions', '1'); |
||||
|
ini_set('assert.exception', 'true'); |
||||
|
error_reporting(\E_ALL); |
||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php'; |
||||
|
|
||||
|
define('CWD', getcwd()); |
@ -0,0 +1,86 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* @license MIT |
||||
|
* Copyright 2022 Dustin Wilson, et al. |
||||
|
* See LICENSE and AUTHORS files for details |
||||
|
*/ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace MensBeam\Logger\Test; |
||||
|
use MensBeam\Logger, |
||||
|
MensBeam\Logger\StreamHandler, |
||||
|
Psr\Log\InvalidArgumentException; |
||||
|
|
||||
|
|
||||
|
/** @covers \MensBeam\Logger */ |
||||
|
class TestLogger extends \PHPUnit\Framework\TestCase { |
||||
|
public function testConstructor(): void { |
||||
|
$l = new Logger(); |
||||
|
$this->assertNull($l->getChannel()); |
||||
|
} |
||||
|
|
||||
|
public function testDefaultHandler(): void { |
||||
|
$l = new Logger(); |
||||
|
$h = $l->getHandlers(); |
||||
|
$this->assertEquals(1, count($h)); |
||||
|
$this->assertInstanceOf(StreamHandler::class, $h[0]); |
||||
|
$s = $h[0]->getStream(); |
||||
|
$this->assertIsString($s); |
||||
|
$this->assertSame('php://stdout', $s); |
||||
|
} |
||||
|
|
||||
|
public function testSettingChannelAndHandlers(): void { |
||||
|
$l = new Logger('oooooooooooooooooooooooooooook', new StreamHandler('ook.log'), new StreamHandler('eek.log')); |
||||
|
$h = $l->getHandlers(); |
||||
|
// Should truncate the channel to 30 characters |
||||
|
$this->assertSame('ooooooooooooooooooooooooooooo', $l->getChannel()); |
||||
|
$this->assertEquals(2, count($h)); |
||||
|
$this->assertSame(CWD . '/ook.log', $h[0]->getStream()); |
||||
|
$this->assertSame(CWD . '/eek.log', $h[1]->getStream()); |
||||
|
} |
||||
|
|
||||
|
/** @dataProvider provideLoggingTests */ |
||||
|
public function testLogging(string $levelName): void { |
||||
|
$s = fopen('php://memory', 'r+'); |
||||
|
// Break after first handler to test breaking in Logger->log |
||||
|
$l = new Logger('ook', new StreamHandler(stream: $s, options: [ 'bubbles' => false ]), new StreamHandler($s)); |
||||
|
$l->$levelName('Ook!'); |
||||
|
rewind($s); |
||||
|
$o = stream_get_contents($s); |
||||
|
$this->assertEquals(1, preg_match('/^' . (new \DateTimeImmutable())->format('M d') . ' \d{2}:\d{2}:\d{2} ook ' . strtoupper($levelName) . ' Ook!\n/', $o)); |
||||
|
} |
||||
|
|
||||
|
/** @dataProvider provideErrorTests */ |
||||
|
public function testErrors(string $throwableClassName, \Closure $closure): void { |
||||
|
$this->expectException($throwableClassName); |
||||
|
$closure(new Logger()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public static function provideLoggingTests(): iterable { |
||||
|
foreach ([ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ] as $l) { |
||||
|
yield [ $l ]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static function provideErrorTests(): iterable { |
||||
|
$iterable = [ |
||||
|
[ |
||||
|
\TypeError::class, |
||||
|
function (Logger $l): void { |
||||
|
$l->log(3.14, 'Ook!'); |
||||
|
} |
||||
|
], |
||||
|
[ |
||||
|
InvalidArgumentException::class, |
||||
|
function (Logger $l): void { |
||||
|
$l->log(42, 'Ook!'); |
||||
|
} |
||||
|
] |
||||
|
]; |
||||
|
|
||||
|
foreach ($iterable as $i) { |
||||
|
yield $i; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" |
||||
|
beStrictAboutOutputDuringTests="true" |
||||
|
beStrictAboutTestsThatDoNotTestAnything="true" |
||||
|
bootstrap="bootstrap.php" |
||||
|
cacheDirectory=".phpunit.cache" |
||||
|
colors="true" |
||||
|
executionOrder="defects" |
||||
|
requireCoverageMetadata="true" |
||||
|
> |
||||
|
<testsuites> |
||||
|
<testsuite name="Main"> |
||||
|
<directory prefix="Test" suffix=".php">./cases</directory> |
||||
|
</testsuite> |
||||
|
</testsuites> |
||||
|
<coverage> |
||||
|
<include> |
||||
|
<directory suffix=".php">../lib</directory> |
||||
|
</include> |
||||
|
</coverage> |
||||
|
</phpunit> |
Loading…
Reference in new issue