432 lines
16 KiB
PHP
432 lines
16 KiB
PHP
<?php
|
||
|
||
namespace app\utils;
|
||
|
||
use app\config\Config;
|
||
use support\Log;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 日志工具类(仅提供静态方法)
|
||
*/
|
||
class Logger
|
||
{
|
||
/**
|
||
* 获取日志实例(核心入口)
|
||
* @param string $name 日志器名称,默认 '__default'
|
||
* @return LoggerIns
|
||
*/
|
||
public static function new(string $name = '__default'): LoggerIns
|
||
{
|
||
return new LoggerIns($name);
|
||
}
|
||
|
||
/**
|
||
* 生成自适应宽度的文本框(精准空格版,绝对对齐)
|
||
* @param array $rows 文本框内的行内容数组
|
||
* @param string $borderChar 边框字符(默认横线)
|
||
* @return string 格式化后的文本框字符串
|
||
*/
|
||
public static function generateTextBox(array $rows, string $borderChar = '-'): string
|
||
{
|
||
// 过滤空行
|
||
$validRows = array_filter($rows, fn($row) => trim($row) !== '');
|
||
if (empty($validRows)) return '';
|
||
|
||
// 计算最长行的字符长度(核心:按实际字符数计算)
|
||
$maxContentLen = max(array_map('mb_strlen', $validRows));
|
||
|
||
// 格式化每行:左侧固定前缀,右侧补空格至最长长度
|
||
$formattedRows = array_map(function ($row) use ($maxContentLen) {
|
||
$currentLen = mb_strlen($row);
|
||
$padding = $maxContentLen - $currentLen; // 精准计算需要补充的空格数
|
||
return '| ' . $row . str_repeat(' ', max(0, $padding)) . ' |';
|
||
}, $validRows);
|
||
|
||
// 计算边框总宽度(格式化后最长行的长度)
|
||
$borderLen = max(array_map('strlen', $formattedRows));
|
||
$borderLine = str_repeat($borderChar, $borderLen);
|
||
|
||
// 空行(和内容行宽度一致)
|
||
$emptyLine = '| ' . str_repeat(' ', $maxContentLen) . ' |';
|
||
|
||
// 拼接文本框
|
||
return "\n{$borderLine}\n{$emptyLine}\n" . implode("\n", $formattedRows) . "\n{$emptyLine}\n{$borderLine}";
|
||
}
|
||
|
||
/**
|
||
* 静态信息级别日志
|
||
* @param string $message 日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @param string $name 日志器名称
|
||
*/
|
||
public static function info(string $message, array|Throwable $context = [], string $name = '__default'): void
|
||
{
|
||
self::new($name)->info($message, $context);
|
||
}
|
||
|
||
/**
|
||
* 静态调试级别日志
|
||
* @param string $message 日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @param string $name 日志器名称
|
||
*/
|
||
public static function debug(string $message, array|Throwable $context = [], string $name = '__default'): void
|
||
{
|
||
self::new($name)->debug($message, $context);
|
||
}
|
||
|
||
/**
|
||
* 静态warn方法(warning别名)
|
||
* @param string $message 日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @param string $name 日志器名称
|
||
*/
|
||
public static function warn(string $message, array|Throwable $context = [], string $name = '__default'): void
|
||
{
|
||
self::new($name)->warn($message, $context);
|
||
}
|
||
|
||
/**
|
||
* 静态warn方法(warning别名)
|
||
* @param string $message 日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @param string $name 日志器名称
|
||
*/
|
||
public static function warning(string $message, array|Throwable $context = [], string $name = '__default'): void
|
||
{
|
||
self::new($name)->warn($message, $context);
|
||
}
|
||
|
||
/**
|
||
* 静态错误级别日志
|
||
* @param string $message 日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @param string $name 日志器名称
|
||
*/
|
||
public static function error(string $message, array|Throwable $context = [], string $name = '__default'): void
|
||
{
|
||
self::new($name)->error($message, $context);
|
||
}
|
||
|
||
/**
|
||
* 静态通用日志方法
|
||
* @param string $level 日志级别
|
||
* @param string $message 日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @param string $name 日志器名称
|
||
*/
|
||
public static function log(string $level, string $message, array|Throwable $context = [], string $name = '__default'): void
|
||
{
|
||
self::new($name)->log($level, $message, $context);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 日志实例类(仅提供实例方法)
|
||
*/
|
||
class LoggerIns
|
||
{
|
||
public $name = '';
|
||
|
||
/**
|
||
* 构造函数:初始化日志器名称
|
||
* @param string $name 日志器名称,默认 '__default'
|
||
*/
|
||
public function __construct(string $name = '__default')
|
||
{
|
||
$this->name = $name;
|
||
}
|
||
|
||
/**
|
||
* 信息级别日志
|
||
* @param string $message 日志消息(支持 {} 占位符)
|
||
* @param array|Throwable $context 占位符替换参数或异常对象
|
||
*/
|
||
public function info(string $message, array|Throwable $context = []): void
|
||
{
|
||
$processedMessage = $this->buildMessage($message, $context);
|
||
if (empty($processedMessage)) return;
|
||
Log::info($processedMessage);
|
||
}
|
||
|
||
/**
|
||
* 调试级别日志
|
||
* @param string $message 日志消息(支持 {} 占位符)
|
||
* @param array|Throwable $context 占位符替换参数或异常对象
|
||
*/
|
||
public function debug(string $message, array|Throwable $context = []): void
|
||
{
|
||
$processedMessage = $this->buildMessage($message, $context);
|
||
if (empty($processedMessage)) return;
|
||
Log::debug($processedMessage);
|
||
}
|
||
|
||
/**
|
||
* 警告级别日志
|
||
* @param string $message 日志消息(支持 {} 占位符)
|
||
* @param array|Throwable $context 占位符替换参数或异常对象
|
||
*/
|
||
public function warning(string $message, array|Throwable $context = []): void
|
||
{
|
||
$processedMessage = $this->buildMessage($message, $context);
|
||
if (empty($processedMessage)) return;
|
||
Log::warning($processedMessage);
|
||
}
|
||
|
||
/**
|
||
* warn 方法 - warning 的别名
|
||
* @param string $message 日志消息(支持 {} 占位符)
|
||
* @param array|Throwable $context 占位符替换参数或异常对象
|
||
*/
|
||
public function warn(string $message, array|Throwable $context = []): void
|
||
{
|
||
$this->warning($message, $context);
|
||
}
|
||
|
||
/**
|
||
* 错误级别日志
|
||
* @param string $message 日志消息(支持 {} 占位符)
|
||
* @param array|Throwable $context 占位符替换参数或异常对象
|
||
*/
|
||
public function error(string $message, array|Throwable $context = []): void
|
||
{
|
||
$processedMessage = $this->buildMessage($message, $context);
|
||
if (empty($processedMessage)) return;
|
||
Log::error($processedMessage);
|
||
}
|
||
|
||
/**
|
||
* 通用日志方法
|
||
* @param string $level 日志级别 (info/debug/warning/warn/error/fatal)
|
||
* @param string $message 日志消息(支持 {} 占位符)
|
||
* @param array|Throwable $context 占位符替换参数或异常对象
|
||
*/
|
||
public function log(string $level, string $message, array|Throwable $context = []): void
|
||
{
|
||
// 验证并处理日志级别(兼容warn别名)
|
||
$validLevels = ['info', 'debug', 'warning', 'warn', 'error', 'fatal'];
|
||
$level = strtolower($level);
|
||
|
||
// 将warn映射为warning
|
||
if ($level === 'warn') {
|
||
$level = 'warning';
|
||
}
|
||
|
||
if (!in_array($level, $validLevels)) {
|
||
$level = 'info'; // 默认使用info级别
|
||
}
|
||
|
||
$processedMessage = $this->buildMessage($message, $context);
|
||
if (empty($processedMessage)) return;
|
||
Log::{$level}($processedMessage);
|
||
}
|
||
|
||
/**
|
||
* 处理日志消息:替换占位符、处理异常、添加名称前缀
|
||
* @param string $message 原始日志消息
|
||
* @param array|Throwable $context 占位符参数或异常对象
|
||
* @return string 处理后的日志消息
|
||
*/
|
||
private function buildMessage(string $message, array|Throwable $context = []): string
|
||
{
|
||
// 1. 先处理占位符替换(无论是否有异常,先处理消息本身)
|
||
$processedMessage = $this->replacePlaceholders($message, is_array($context) ? $context : []);
|
||
|
||
// 2. 处理异常对象:换行输出完整异常内容,优化堆栈缩进
|
||
if ($context instanceof Throwable) {
|
||
// 转换异常为字符串并优化堆栈缩进(制表符)
|
||
$exceptionStr = $this->formatExceptionStackTrace((string)$context);
|
||
// 使用 PHP_EOL 保证跨系统换行
|
||
$processedMessage .= PHP_EOL . $exceptionStr;
|
||
}
|
||
|
||
// 3. 添加名称前缀 [name]
|
||
$namePrefix = "<RP_NAME:{$this->name}>";
|
||
$result = $namePrefix . $processedMessage;
|
||
if ($this->isFilter()) return "";
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 判断当前日志是否需要过滤(核心过滤逻辑)
|
||
* @return bool true=需要过滤,false=保留日志
|
||
*/
|
||
private function isFilter(): bool
|
||
{
|
||
$logFilter = Config::getInstance()->logFilter;
|
||
// 无过滤规则时直接返回false(不过滤)
|
||
if (empty($logFilter)) {
|
||
return false;
|
||
}
|
||
|
||
// 1. 解析调用堆栈,获取业务代码的类名/方法名
|
||
$stackInfo = $this->parseBusinessStackInfo();
|
||
$className = $stackInfo['class'] ?? $this->name ?? '';
|
||
$methodName = $stackInfo['method'] ?? '';
|
||
|
||
// 2. 通配符匹配函数
|
||
$matchWildcard = function (string $pattern, string $value): bool {
|
||
// 空规则特殊处理:如:debug 拆分后类规则为空,代表匹配所有类
|
||
if ($pattern === '') {
|
||
return true;
|
||
}
|
||
// 将通配符*转换为正则的.*,并转义其他特殊字符
|
||
$regexPattern = preg_quote($pattern, '/');
|
||
$regexPattern = str_replace('\*', '.*', $regexPattern);
|
||
// 正则全程匹配
|
||
return preg_match('/^' . $regexPattern . '$/', $value) === 1;
|
||
};
|
||
|
||
// 3. 遍历过滤规则,匹配则返回true(需要过滤)
|
||
foreach ($logFilter as $filter) {
|
||
// 跳过非法过滤规则
|
||
if (!is_string($filter)) {
|
||
continue;
|
||
}
|
||
// 拆分规则为类规则和方法规则(最多拆2部分,避免方法名含:)
|
||
[$filterClassRule, $filterMethodRule] = array_pad(explode(':', $filter, 2), 2, '*');
|
||
|
||
// 匹配类名规则(支持通配符*)
|
||
$isClassMatch = $matchWildcard($filterClassRule, $className);
|
||
// 匹配方法名规则(支持通配符*)
|
||
$isMethodMatch = $matchWildcard($filterMethodRule, $methodName);
|
||
|
||
// 类和方法都匹配时,返回true(过滤该日志)
|
||
if ($isClassMatch && $isMethodMatch) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 无匹配规则,返回false(保留日志)
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 解析调用堆栈,获取实际业务代码的类名和方法名
|
||
* @return array ['class' => 业务类名, 'method' => 业务方法名]
|
||
*/
|
||
private function parseBusinessStackInfo(): array
|
||
{
|
||
$stackInfo = ['class' => '', 'method' => ''];
|
||
// 获取调用栈(忽略参数,避免性能损耗,取前10层足够)
|
||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
|
||
|
||
// 跳过的日志相关类/方法(根据实际项目调整)
|
||
$skipPatterns = [
|
||
'class' => [
|
||
'Monolog\\', // 跳过Monolog核心类
|
||
__CLASS__, // 跳过当前类自身
|
||
'app\\logger\\', // 跳过大日志封装类(根据你的项目调整)
|
||
],
|
||
'method' => [
|
||
'buildMessage', // 跳过当前方法
|
||
'isFilter', // 跳过过滤方法
|
||
'parseBusinessStackInfo', // 跳过堆栈解析方法
|
||
'replacePlaceholders',// 跳過占位符替换方法
|
||
'formatExceptionStackTrace', // 跳过异常格式化方法
|
||
'log', 'error', 'info', 'warning', 'debug' // 跳过日志级别方法
|
||
]
|
||
];
|
||
|
||
// 遍历调用栈,找到第一个业务代码层
|
||
foreach ($trace as $step) {
|
||
// 跳过无文件/无方法的栈帧
|
||
if (!isset($step['file']) || !isset($step['function'])) {
|
||
continue;
|
||
}
|
||
|
||
// 跳过日志相关类
|
||
$currentClass = $step['class'] ?? '';
|
||
$isSkipClass = false;
|
||
foreach ($skipPatterns['class'] as $pattern) {
|
||
if ($pattern && str_contains($currentClass, $pattern)) {
|
||
$isSkipClass = true;
|
||
break;
|
||
}
|
||
}
|
||
if ($isSkipClass) {
|
||
continue;
|
||
}
|
||
|
||
// 跳过日志相关方法
|
||
$currentMethod = $step['function'];
|
||
if (in_array($currentMethod, $skipPatterns['method'])) {
|
||
continue;
|
||
}
|
||
|
||
// 解析业务类名(简化为项目内相对路径,和原逻辑一致)
|
||
$projectRoot = defined('BASE_PATH') ? BASE_PATH : (function_exists('base_path') ? base_path() : '');
|
||
$file = $step['file'];
|
||
if ($projectRoot && str_starts_with($file, $projectRoot)) {
|
||
$file = substr($file, strlen($projectRoot) + 1);
|
||
// 转换为类名格式(app/controller/Index.php → app.controller.Index)
|
||
$class = str_replace(['.php', '\\', '/'], ['', '.', '.'], $file);
|
||
} else {
|
||
$class = $currentClass ?: basename($file, '.php');
|
||
}
|
||
|
||
// 赋值并终止遍历(找到第一个业务层即可)
|
||
$stackInfo['class'] = trim($class);
|
||
$stackInfo['method'] = $currentMethod;
|
||
break;
|
||
}
|
||
|
||
return $stackInfo;
|
||
}
|
||
|
||
/**
|
||
* 格式化异常堆栈:让Stack trace下的每一行添加制表符缩进
|
||
* @param string $exceptionStr 原始异常字符串
|
||
* @return string 格式化后的异常字符串
|
||
*/
|
||
private function formatExceptionStackTrace(string $exceptionStr): string
|
||
{
|
||
// 正则匹配 Stack trace: 后的所有行,并在每行前添加制表符
|
||
$pattern = '/(Stack trace:\s*)(.*)$/s';
|
||
return preg_replace_callback($pattern, function ($matches) {
|
||
// $matches[1] 是 "Stack trace: "
|
||
// $matches[2] 是堆栈内容
|
||
$stackTrace = $matches[2];
|
||
// 将堆栈的每一行(#开头)前添加 制表符(\t)
|
||
$formattedStack = preg_replace('/(#\d+)/', "\t$1", $stackTrace);
|
||
return $matches[1] . $formattedStack;
|
||
}, $exceptionStr);
|
||
}
|
||
|
||
/**
|
||
* 内部方法:替换 {} 占位符
|
||
* @param string $message 原始消息
|
||
* @param array $context 替换参数
|
||
* @return string 替换后的消息
|
||
*/
|
||
private function replacePlaceholders(string $message, array $context = []): string
|
||
{
|
||
if (empty($context)) {
|
||
return $message;
|
||
}
|
||
|
||
// 将上下文参数转换为安全的字符串
|
||
$replacements = array_map(function ($value) {
|
||
if ($value === null) {
|
||
return 'null';
|
||
}
|
||
if (is_bool($value)) {
|
||
return $value ? 'true' : 'false';
|
||
}
|
||
if (is_array($value) || is_object($value)) {
|
||
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR);
|
||
}
|
||
return (string)$value;
|
||
}, $context);
|
||
|
||
// 按顺序替换 {} 占位符
|
||
$index = 0;
|
||
$result = preg_replace_callback('/\{\}/', function () use (&$index, $replacements) {
|
||
return isset($replacements[$index]) ? $replacements[$index++] : '{}';
|
||
}, $message);
|
||
|
||
// 替换失败时返回原始消息
|
||
return $result ?: $message;
|
||
}
|
||
} |