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