[ 'handlers' => [ // 控制台输出处理器 [ 'class' => StreamHandler::class, 'constructor' => [ 'php://stdout', Config::getInstance()->logLevel, ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, /** * 格式化日志输出 * %start%: 标记日志颜色 * %end%: 结束标记颜色 * %logger%: 日志记录器名称 * %L: 行号 * %M: 方法名 * %P: 进程ID * %thread%: 线程ID * %Level%: 日志级别(带有占位符) * %level_name%: 日志级别名称 * %level%: 日志等级数字 * %message%: 日志内容 * %datetime%: 时间 */ 'constructor' => [ "%start%%datetime% [%thread%] %Level% %logger%:%L% - %message%%end%\n", 'Y-m-d H:i:s', true ], ], ], // 默认文件输出处理器 [ 'class' => RotatingFileHandler::class, 'constructor' => [ runtime_path() . '/logs/webman.log', Config::getInstance()->logRotationTimeByDay, Config::getInstance()->logLevel, ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ "%datetime% [%thread%] %Level% %logger%:%L% - %message%\n", 'Y-m-d H:i:s', true ], ], ], // Error级别单独文件处理器 [ 'class' => FilterHandler::class, 'constructor' => [ new RotatingFileHandler( runtime_path() . '/logs/error.log', Config::getInstance()->errorLogRotationTimeByDay, Logger::DEBUG ), Logger::ERROR, Logger::EMERGENCY ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ "%datetime% [%thread%] %Level% %logger%:%L% - %message%\n", 'Y-m-d H:i:s', true ], ], ], ], // 全局处理器:手动解析调用栈,精准定位业务代码 'processors' => [ // 1. 注入进程ID(生成线程名) new ProcessIdProcessor(), // 2. 手动解析调用栈,获取实际业务代码位置 + 日志过滤 function ($record) { $message = $record['message']; // 线程名 $processId = $record['extra']['process'] ?? 0; $record['thread'] = "thread-{$processId}"; // 手动解析调用栈,定位实际业务代码 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); // 获取调用栈(前10层) $file = $record['channel']; $function = ''; $line = '0'; // 遍历调用栈,跳过日志类相关的层,找到业务代码层 foreach ($trace as $step) { // 跳过 webman 日志类、monolog 相关的调用层 if (isset($step['file']) && str_contains($step['file'], 'support') === false && str_contains($step['file'], 'monolog') === false && str_contains($step['file'], 'Logger') === false && str_contains($step['file'], 'vendor') === false ) { // 获取文件路径(简化为项目内相对路径) $file = $step['file']; $projectRoot = base_path(); if (str_starts_with($file, $projectRoot)) { $file = substr($file, strlen($projectRoot) + 1); } $file = str_replace('.php', '', $file); $file = str_replace('\\', '.', $file); // 获取方法名 $function = $step['function']; // 获取行 $line = $step['line'] ?? '0'; break; // 找到第一个业务代码层就停止 } } $record['M'] = $function; $record['L'] = $line; $record['P'] = $record['extra']['process_id']; // 解析 标签 $pattern = '//si'; if (preg_match_all($pattern, $message, $matches) && !empty($matches[1])) { $record['logger'] = $matches[1][0]; $record['message'] = preg_replace($pattern, '', $message); if ($matches[1][0] == '__default') $record['logger'] = $file; } else { $record['logger'] = $file; $record['message'] = $message; } // 解析 SQL_LOG 相关标签 $tagPatterns = [ 'logger' => '/(.*?)<\/SQL_LOG_F>/s', // 文件路径 'L' => '/(.*?)<\/SQL_LOG_L>/s', // 行号 'M' => '/(.*?)<\/SQL_LOG_M>/s' // 方法名 ]; $tempMessage = $record['message']; // 基于处理后的message继续解析 foreach ($tagPatterns as $key => $pattern) { if (preg_match($pattern, $tempMessage, $matches)) { $record[$key] = $matches[1]; $tempMessage = preg_replace($pattern, '', $tempMessage); // 移除当前标签 $record['message'] = trim($tempMessage); } } // 日志级别颜色映射 $levelColorMap = [ Logger::EMERGENCY => "\033[41m\033[37m", // 最高级别:红色底色+白色字体 Logger::ALERT => "\033[41m\033[37m", // 警报:红色底色+白色字体 Logger::CRITICAL => "\033[41m\033[37m", // 严重:红色底色+白色字体 Logger::ERROR => "\033[31m", // 错误:红色字体(无底色) Logger::WARNING => "\033[33m", // 警告:深黄色字体 Logger::INFO => "\033[32m", // 信息:绿色字体 Logger::DEBUG => "\033[34m", // 调试:蓝色字体 ]; $record['start'] = "\033[0m"; $record['end'] = "\033[0m"; foreach ($levelColorMap as $level => $color) { if ($record['level'] >= $level) { $record['start'] = $color; $record['end'] = "\033[0m"; break; } } // 获取日志名称 $logger = $record['logger'] ?? ''; if (str_starts_with($logger, 'plugin.')) { $logger = preg_replace('/^plugin\.([^.]+)\./', '[$1] ', $logger); } $record['logger'] = $logger; $record['Level'] = str_pad(strtoupper($record['level_name']), 5); return $record; } ] ], ];