16 Commits

Author SHA1 Message Date
crazywhalecc
30bee20a0a fix: remove @ error suppression from echo/fwrite/fflush calls
`@echo` is invalid PHP syntax — echo is a language construct,
not a function, so the @ operator cannot be applied to it. This
causes a parse error on some PHP versions/runtimes.

The recursion guard ($in_log) added in 1.1.7 already prevents
infinite loops when STDOUT/STDERR is broken, so @ suppression
is unnecessary. Remove @ from echo, fwrite, and fflush calls.

Version bumped from 1.1.8 to 1.1.9.
2026-06-17 14:59:32 +08:00
Jerry Ma
d13c8fb85f Update ConsoleLogger.php 2026-06-17 15:53:31 +09:00
crazywhalecc
c21ddda192 chore: widen PHP version constraint to 8.5 and bump dev deps
- Add PHP 8.2/8.3/8.4/8.5 to version constraint
- Bump php-cs-fixer from ^3.2 to ^3.64
- Bump phpstan from ^1.1 to ^1.12
- Drop phpunit ^8.5 (EOL), keep ^9.0 only
2026-06-17 14:53:05 +08:00
crazywhalecc
d79edcef53 fix: add recursion guard to prevent 100% CPU when STDOUT/STDERR is broken
When the terminal (IDE) is closed without stopping the framework first,
the PTY is destroyed and STDOUT/STDERR become broken file descriptors.
Any subsequent log call via echo/fwrite fails with E_WARNING, the error
handler catches it and calls the logger again, creating a recursive loop
that consumes 100% CPU.

Changes:
- Add static $in_log recursion guard flag to ConsoleLogger
- Wrap log output (echo/fwrite/fflush) with the guard and @ suppression
- Bump version from 1.1.6 to 1.1.7
2026-06-17 14:46:27 +08:00
crazywhalecc
ac5fa59428 Add long level name 2026-05-06 14:47:06 +08:00
crazywhalecc
26dc5fa8c3 Enhance output handling in ConsoleLogger to use STDERR for specific log levels 2025-12-16 10:56:19 +08:00
crazywhalecc
e75fa01ca5 Enhance output handling in ConsoleLogger to use STDERR for specific log levels 2025-12-16 10:56:09 +08:00
crazywhalecc
b28c0c26d5 Add setLevel function 2025-11-08 23:38:01 +08:00
crazywhalecc
2551b3e1db phpstan 2025-08-02 21:44:27 +08:00
crazywhalecc
81ab0b98f6 Update to 1.1.3 2025-08-02 21:43:12 +08:00
crazywhalecc
1d7427abd8 Add short level format 2025-04-24 10:02:28 +08:00
sunxyw
1b7e343493 Merge pull request #7 from zhamao-robot/fix-no-terminal-size-without-io
修復无交互环境下无法获取终端尺寸
2023-03-09 23:41:10 +08:00
sunxyw
97ee152121 update version 2023-03-09 23:40:30 +08:00
sunxyw
11156c65a2 fix no terminal size without io 2023-03-09 22:41:30 +08:00
crazywhalecc
5158a0ee2a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/ZM/Logger/ConsoleLogger.php
2022-11-05 22:08:03 +08:00
crazywhalecc
92d281262c add stream output support and "decorated" setting 2022-11-05 22:06:35 +08:00
3 changed files with 133 additions and 41 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "zhamao/logger",
"description": "Another Console Logger for CLI Applications",
"description": "Another Colorful Console Logger for CLI Applications",
"type": "library",
"license": "Apache-2.0",
"autoload": {
@@ -16,7 +16,7 @@
"authors": [
{
"name": "jerry",
"email": "admin@zhamao.me"
"email": "github@cwcc.me"
},
{
"name": "sunxyw",
@@ -25,7 +25,7 @@
],
"minimum-stability": "stable",
"require": {
"php": "^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1",
"php": "^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5",
"psr/log": "^1 || ^2 || ^3",
"symfony/polyfill-mbstring": "^1.0"
},
@@ -37,9 +37,9 @@
"ext-mbstring": "Use C/C++ extension instead of polyfill will be more efficient"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.2",
"phpstan/phpstan": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.0",
"friendsofphp/php-cs-fixer": "^3.64",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^9.0",
"roave/security-advisories": "dev-latest",
"brainmaestro/composer-git-hooks": "^2.8"
},

View File

@@ -10,7 +10,7 @@ use Psr\Log\LogLevel;
class ConsoleLogger extends AbstractLogger
{
public const VERSION = '1.0.2';
public const VERSION = '1.1.9';
/**
* 日志输出格式
@@ -79,12 +79,59 @@ class ConsoleLogger extends AbstractLogger
*/
protected $log_callbacks = [];
/**
* Stream 写入
*
* @var null|int|resource
*/
protected $stream;
/**
* 递归保护标志 — 防止日志写入失败时错误处理器再次调用日志导致死循环 CPU 100%
*
* @var bool
*/
protected static $in_log = false;
/**
* 是否带颜色
*
* @var bool
*/
protected $decorated;
protected $use_stderr = false;
/**
* 创建一个 ConsoleLogger 实例
*
* @param string $level 日志等级
* @param string $level 日志等级
* @param null|resource $stream
*/
public function __construct(string $level = LogLevel::INFO)
public function __construct(string $level = LogLevel::INFO, $stream = null, bool $decorated = true, bool $use_stderr = false)
{
$this->decorated = $decorated;
self::$log_level = $this->castLogLevel($level);
if (!$stream || !is_resource($stream) || get_resource_type($stream) !== 'stream') {
return;
}
$stat = fstat($stream);
if (!$stat) {
return;
}
if (($stat['mode'] & 0170000) === 0100000) { // whether is regular file
$this->decorated = false;
} else {
$this->decorated
= PHP_OS_FAMILY !== 'Windows' // linux or unix
&& function_exists('posix_isatty')
&& posix_isatty($stream); // whether is interactive terminal
}
$this->stream = $stream;
$this->use_stderr = $use_stderr;
}
public function setLevel(string $level): void
{
self::$log_level = $this->castLogLevel($level);
}
@@ -128,9 +175,9 @@ class ConsoleLogger extends AbstractLogger
*/
public function trace(): void
{
$log = "Stack trace:\n";
$log = 'Stack trace:' . PHP_EOL;
$trace = debug_backtrace();
//array_shift($trace);
// array_shift($trace);
foreach ($trace as $i => $t) {
if (!isset($t['file'])) {
$t['file'] = 'unknown';
@@ -139,13 +186,24 @@ class ConsoleLogger extends AbstractLogger
$t['line'] = 0;
}
$log .= "#{$i} {$t['file']}({$t['line']}): ";
/* @phpstan-ignore-next-line */
if (isset($t['object']) && is_object($t['object'])) {
$log .= get_class($t['object']) . '->';
}
$log .= "{$t['function']}()\n";
$log .= "{$t['function']}()" . PHP_EOL;
}
if ($this->decorated) {
$log = $this->colorize($log, $this->castLogLevel(LogLevel::DEBUG));
}
// use stream
if ($this->stream) {
fwrite($this->stream, $log);
fflush($this->stream);
} else {
// use plain text output
echo $log;
}
$log = $this->colorize($log, $this->castLogLevel(LogLevel::DEBUG));
echo $log;
}
/**
@@ -161,43 +219,75 @@ class ConsoleLogger extends AbstractLogger
return ConsoleColor::apply($styles, $string)->__toString();
}
/**
* {@inheritDoc}
*/
public function log($level, $message, array $context = []): void
{
$level = $this->castLogLevel($level);
$log_replace = [
'%date%' => date(self::$date_format),
'%level_long%' => strtoupper(self::$levels[$level]),
'%level%' => strtoupper(substr(self::$levels[$level], 0, 4)),
'%body%' => $message,
'%level_short%' => strtoupper(substr(self::$levels[$level], 0, 1)),
];
$output = str_replace(array_keys($log_replace), array_values($log_replace), self::$format);
$output = $this->interpolate($output, array_merge($this->static_context, $context));
foreach ($this->log_callbacks as $callback) {
if ($callback($level, $output, $message, $context, $this->shouldLog($level)) === false) {
return;
}
}
if (!$this->shouldLog($level)) {
return;
}
$output = str_replace(
['%date%', '%level%', '%body%'],
[date(self::$date_format), strtoupper(substr(self::$levels[$level], 0, 4)), $message],
self::$format
);
$output = $this->interpolate($output, array_merge($this->static_context, $context));
foreach ($this->log_callbacks as $callback) {
if ($callback($level, $output, $message, $context) === false) {
return;
}
if ($this->decorated) {
$output = $this->colorize($output, $level) . PHP_EOL;
} else {
$output = $output . PHP_EOL;
}
// 递归保护:如果上一次日志写入触发了错误(如终端关闭后 STDOUT/STDERR 破损),
// 跳过本次输出,防止错误处理器递归调用导致 CPU 100% 空耗
if (self::$in_log) {
return;
}
self::$in_log = true;
try {
// use stream
if ($this->stream) {
fwrite($this->stream, $output);
fflush($this->stream);
} else {
if ($level <= 4 && $this->use_stderr) {
fwrite(STDERR, $output);
} else {
// use plain text output
echo $output;
}
}
} finally {
self::$in_log = false;
}
}
echo $this->colorize($output, $level) . "\n";
public function setDecorated(bool $decorated): void
{
$this->decorated = $decorated;
}
/**
* 转换日志等级
*/
private function castLogLevel(string $level): int
protected function castLogLevel(string $level): int
{
if (in_array($level, self::$levels, true)) {
return array_flip(self::$levels)[$level];
}
throw new InvalidArgumentException('无效的日志等级');
throw new InvalidArgumentException('Invalid log level: ' . $level);
}
/**
@@ -205,7 +295,7 @@ class ConsoleLogger extends AbstractLogger
*
* @param mixed $item 日志内容
*/
private function stringify($item): string
protected function stringify($item): string
{
switch (true) {
case is_callable($item):
@@ -219,10 +309,7 @@ class ConsoleLogger extends AbstractLogger
case is_string($item):
return $item;
case is_array($item):
if (extension_loaded('json')) {
return 'array' . json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_LINE_TERMINATORS);
}
return var_export($item, true);
return 'array' . (extension_loaded('json') ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_LINE_TERMINATORS) : '');
case is_object($item):
return get_class($item);
case is_resource($item):
@@ -242,7 +329,7 @@ class ConsoleLogger extends AbstractLogger
/**
* 判断是否应该记录该等级日志
*/
private function shouldLog(int $level): bool
protected function shouldLog(int $level): bool
{
return $level <= self::$log_level;
}
@@ -253,7 +340,7 @@ class ConsoleLogger extends AbstractLogger
* @param string $message 日志内容
* @param array $context 变量列表
*/
private function interpolate(string $message, array $context = []): string
protected function interpolate(string $message, array $context = []): string
{
$replace = [];
foreach ($context as $key => $value) {

View File

@@ -137,8 +137,8 @@ class TablePrinter
$line_data[$current_line] = [
'used' => $valid_width - $k_len - 2 - $partial_v_len,
'can_put_second' => false,
'lines' => $k . ': ' .
ConsoleColor::apply([$this->value_color], $partial_v . str_pad('', $valid_width - $k_len - 2 - $partial_v_len, '.')),
'lines' => $k . ': '
. ConsoleColor::apply([$this->value_color], $partial_v . str_pad('', $valid_width - $k_len - 2 - $partial_v_len, '.')),
];
++$current_line;
// 下一个参数
@@ -237,7 +237,12 @@ class TablePrinter
}
} else {
$size = exec('stty size 2>/dev/null');
$size = (int) explode(' ', trim($size))[1];
// in case stty is not available
if (empty($size)) {
$size = 0;
} else {
$size = (int) explode(' ', trim($size))[1];
}
}
if (empty($size)) {
return $this->terminal_size = 79;