- 后端
- 微服务
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
hyperf/logger是 Hyperf 框架的日志组件,它遵循psr/logger标准接口,并以monolog/monolog作为底层驱动,为常驻内存、高并发的协程环境提供了安全、灵活、可高度定制的日志能力。本文将以 docs/id/logger.md 为主体脉络,结合src/logger组件源码与测试用例,带你从安装配置、基础用法、Monolog 核心概念,一路深入到静态日志门面、多 Handler 组合、按日期切割、Request 级统一日志等生产级实践,读完即可在项目中落地一套完整可用的日志方案。
组件定位与设计思路
hyperf/logger是基于psr/logger标准实现的日志组件,默认使用monolog/monolog作为驱动。在hyperf-skeleton骨架项目中,默认已经提供了若干 logger 配置,使用的是Monolog\Handler\StreamHandler。
一个关键点在于协程安全:由于 Swoole 已经对fopen、fwrite等函数提供了协程化支持,因此在协程环境下可以放心使用这些函数,只要不把useLocking参数设置为true即可。
从组件源码看,hyperf/logger对 Monolog 的封装非常薄,核心只做了三件事:
- 提供一个继承自
Monolog\Logger的Hyperf\Logger\Logger类,同时实现Hyperf\Contract\StdoutLoggerInterface,见 src/logger/src/Logger.php,并在构造时关闭了 Monolog 的循环日志检测(useLoggingLoopDetection(false)); - 提供
LoggerFactory工厂,负责从配置中心读取logger配置并装配出带 Handler、Formatter、Processor 的 Logger 实例,见 src/logger/src/LoggerFactory.php; - 通过
ConfigProvider把默认配置文件发布到项目的config/autoload/logger.php,见 src/logger/src/ConfigProvider.php。
组件依赖方面,src/logger/composer.json 要求 PHP >= 8.2、monolog/monolog: ^3.1、psr/log: ^2.0 || ^3.0,也就是说本文所讲的配置语法均基于Monolog 3.x(如使用Monolog\Level枚举而不是旧的整数常量)。
安装与配置发布
在项目中安装组件只需一行命令:
composer require hyperf/logger安装完成后,由于组件在ConfigProvider中声明了publish配置(来源为src/logger/publish/logger.php,目标为BASE_PATH . '/config/autoload/logger.php'),执行php bin/hyperf.php vendor:publish hyperf/logger即可把默认配置发布到项目。hyperf-skeleton骨架项目中默认已带有 logger 配置,一个最简形态如下:
<?php return [ 'default' => [ 'handler' => [ 'class' => \Monolog\Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => \Monolog\Level::Debug, ], ], 'formatter' => [ 'class' => \Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => null, 'allowInlineLineBreaks' => true, ] ], ], ];从当前仓库的发布配置 src/logger/publish/logger.php 可以看到,实际发布的默认配置比上述示例更完整,它采用default+channels的结构,默认 channel 由环境变量LOG_CHANNEL决定(默认stack),并预置了stack、single、daily、stderr、syslog、null六个 channel:
<?php use Monolog\Formatter\LineFormatter; use Monolog\Formatter\SyslogFormatter; use Monolog\Handler\NullHandler; use Monolog\Handler\RotatingFileHandler; use Monolog\Handler\StreamHandler; use Monolog\Handler\SyslogHandler; use Monolog\Level; use Monolog\Processor\PsrLogMessageProcessor; use function Hyperf\Support\env; return [ // Default Log Channel 'default' => env('LOG_CHANNEL', 'stack'), // Log Channels 'channels' => [ 'stack' => [ 'handlers' => explode(',', (string) env('LOG_STACK', 'single')), ], 'single' => [ 'handler' => [ 'class' => StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Level::Debug, ], ], 'formatter' => [ 'class' => LineFormatter::class, 'constructor' => [], ], 'processors' => [], ], 'daily' => [ 'handler' => [ 'class' => RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Level::Debug, ], ], 'formatter' => [ 'class' => LineFormatter::class, 'constructor' => [], ], 'processors' => [], ], 'stderr' => [ 'handler' => [ 'class' => StreamHandler::class, 'constructor' => [ 'stream' => 'php://stderr', 'level' => Level::Debug, ], ], 'formatter' => [ 'class' => LineFormatter::class, 'constructor' => [], ], 'processors' => [ PsrLogMessageProcessor::class, ], ], 'syslog' => [ 'handler' => [ 'class' => SyslogHandler::class, 'constructor' => [ 'level' => Level::Debug, 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), ], ], 'formatter' => [ 'class' => SyslogFormatter::class, 'constructor' => [], ], 'processors' => [], ], 'null' => [ 'handler' => ['class' => NullHandler::class], ], ], ];这份配置本身就是最好的参考:stack通过handlers数组引用其他 channel 名称实现多 Handler 组合,single写单文件,daily按日期轮转,stderr输出到标准错误流,syslog走系统日志,null则丢弃所有日志。
基础用法:通过 LoggerFactory 获取 Logger
在业务代码中,通常通过构造函数注入Hyperf\Logger\LoggerFactory,再调用get()获取Psr\Log\LoggerInterface实例:
<?php declare(strict_types=1); namespace App\Service; use Psr\Log\LoggerInterface; use Hyperf\Logger\LoggerFactory; class DemoService { protected LoggerInterface $logger; public function __construct(LoggerFactory $loggerFactory) { // 第一个参数是日志名(即 channel 名),第二个参数是 config/autoload/logger.php 中的 key $this->logger = $loggerFactory->get('log', 'default'); } public function method() { // 做一些事情。 $this->logger->info("Your log message."); } }这里要理清两个参数的含义:
- 第一个参数
$name:传给 MonologLogger构造函数的 channel 名称,会出现在日志行的%channel%字段中; - 第二个参数
$channel:配置文件config/autoload/logger.php中的配置项 key(如default),决定使用哪一组 Handler/Formatter/Processor 装配。不传时默认取配置中logger.default指定的值。
从 LoggerFactory.php 的源码可以看到,get()内部维护了$this->loggers[$channel][$name]缓存,同一个 channel 下相同 name 的 Logger 只会创建一次;真正装配逻辑在make()中完成:
public function make(string $name = 'hyperf', ?string $channel = null): LoggerInterface { $channel ??= $this->config->get('logger.default', 'default'); $key = 'logger.channels.' . $channel; if (! $channel || ! $this->config->has($key)) { throw new InvalidConfigException(sprintf('Logger config[%s] is not defined.', $channel)); } $config = $this->config->get($key, []); if (is_callable($config)) { $config = $config($name); } $handlers = $this->handlers($config); $processors = $this->processors($config); return make(Logger::class, [ 'name' => $name, 'handlers' => $handlers, 'processors' => $processors, ]); }值得注意的细节:
- 配置项可以是一个可调用对象(闭包),它会在创建时收到
$name参数并返回配置数组——这在测试中用于按日志名动态生成文件路径,见 LoggerFactoryTest.php 中的callablechannel; - 构造函数中做了旧版配置兼容:如果配置中没有
logger.channels,或logger.default本身是数组,则把整个logger配置重写为default+channels的新结构,见 LoggerFactory.php。
Monolog 基础概念:Channel、Handler、Formatter、Processor
要驾驭 Hyperf 的日志体系,必须先理解 Monolog 的四个核心概念。我们通过一段原生 Monolog 代码来直观认识:
use Monolog\Formatter\LineFormatter; use Monolog\Handler\FirePHPHandler; use Monolog\Handler\StreamHandler; use Monolog\Logger; // 创建一个 Channel,参数 'log' 就是 Channel 名称 $log = new Logger('log'); // 创建两个 Handler,分别对应变量 $stream 与 $fire $stream = new StreamHandler('test.log', Logger::WARNING); $fire = new FirePHPHandler(); // 指定日期格式为 "Y-m-d H:i:s" $dateFormat = "Y n j, g:i a"; // 指定日志格式为 "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n" $output = "%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n"; // 根据日期格式和日志格式创建 Formatter $formatter = new LineFormatter($output, $dateFormat); // 将 Formatter 设置到 Handler 上 $stream->setFormatter($formatter); // 把 Handler 压入 Channel 的 Handler 队列 $log->pushHandler($stream); $log->pushHandler($fire); // 克隆一个新的日志 channel $log2 = $log->withName('log2'); // 向日志中添加记录 $log->warning('Foo'); // 向记录中添加额外数据 // 1. log context $log->error('new user', ['username' => 'daydaygo']); // 2. processor $log->pushProcessor(function ($record) { $record['extra']['dummy'] = 'hello'; return $record; }); $log->pushProcessor(new \Monolog\Processor\MemoryPeakUsageProcessor()); $log->alert('czl');对这段代码做如下总结:
- 首先实例化
Logger并指定名称,该名称对应channel; - 一个
Logger可以绑定多个Handler,当Logger记录日志时,会把处理工作委托给这些Handler; Handler可以指定处理哪些日志级别,例如Logger::WARNING只处理>= Logger::WARNING的日志;- 负责格式化日志的是
Formatter,把Formatter设置并绑定到对应的Handler上; - 一条日志由
"%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n"组成; - 注意区分日志中追加的
context与extra:context是用户记录日志时自行传入的附加数据,更灵活;extra由绑定在Logger上的Processor固定追加,更适合收集通用信息。
高级用法
封装一个全局静态Log类
如果你更习惯其他框架那种"静态方法打日志"的写法,可以在App命名空间下创建一个Log类,通过静态方法拿到 Logger:
注意:使用时要避免让
$name与请求绑定在一起。例如用$request_id作为 logger 名称,会导致 Factory 在请求级别缓存 logger 对象,造成严重的内存泄漏。
namespace App; use Hyperf\Logger\LoggerFactory; use Hyperf\Context\ApplicationContext; class Log { public static function get(string $name = 'app') { return ApplicationContext::getContainer()->get(LoggerFactory::class)->get($name); } }默认情况下它使用名为app的 Channel 记录日志,也可以通过Log::get($name)获取不同 Channel 的 Logger。这一切都由强大的Container替你完成。
文档中还提到可以用__callStatic魔术方法实现按级别静态调用(如Log::info(...)),核心思路一致:把静态调用转发到从容器取出的 Logger 实例上,再调用对应级别方法。
让框架日志(stdout)也走 Monolog
默认情况下,框架组件产生的日志由Hyperf\Contract\StdoutLoggerInterface的实现类Hyperf\Framework\Logger\StdoutLogger支撑,见 src/framework/src/Logger/StdoutLogger.php。这个类只是通过ConsoleOutput::writeln()把信息输出到标准输出(stdout)——即运行 Hyperf 的终端,实际上并没有使用 monolog。它还支持通过配置控制哪些级别允许输出(log_level白名单)。
如果希望保持日志出口一致、统一走 Monolog,仍然通过强大的Container完成:
第一步,实现一个StdoutLoggerFactory(关于 Factory 的更多用法见 Dependency Injection):
<?php declare(strict_types=1); namespace App; use Psr\Container\ContainerInterface; class StdoutLoggerFactory { public function __invoke(ContainerInterface $container) { return Log::get('sys'); } }第二步,声明依赖关系:在config/autoload/dependencies.php中,把StdoutLoggerInterface绑定到StdoutLoggerFactory,这样所有使用StdoutLoggerInterface的地方都会解析到由该 Factory 实例化的类:
// config/autoload/dependencies.php return [ \Hyperf\Contract\StdoutLoggerInterface::class => \App\StdoutLoggerFactory::class, ];不同环境使用不同的日志格式
上面的用法都围绕 Monolog 的Logger展开,下面来看Handler与Formatter的灵活组合。一个典型的实践是:开发环境输出到终端且保留多行与堆栈,生产环境输出 JSON 方便接入第三方日志平台。
// config/autoload/logger.php $appEnv = env('APP_ENV', 'dev'); if ($appEnv == 'dev') { $formatter = [ 'class' => \Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => "||%datetime%||%channel%||%level_name%||%message%||%context%||%extra%\n", 'allowInlineLineBreaks' => true, 'includeStacktraces' => true, ], ]; } else { $formatter = [ 'class' => \Monolog\Formatter\JsonFormatter::class, 'constructor' => [], ]; } return [ 'default' => [ 'handler' => [ 'class' => \Monolog\Handler\StreamHandler::class, 'constructor' => [ 'stream' => 'php://stdout', 'level' => \Monolog\Level::Info, ], ], 'formatter' => $formatter, ], ];要点如下:
- 默认配置一个名为
default的Handler,其中包含该Handler及其Formatter的信息; - 获取
Logger时,如果没有指定 Handler,底层会自动把defaultHandler 绑定到 Logger 上; dev(开发)环境:日志通过php://stdout输出到标准输出(stdout),且在Formatter中设置allowInlineLineBreaks,便于阅读多行日志;- 非
dev环境:使用JsonFormatter把日志格式化为json,便于投递到第三方日志服务。
按日期切割日志文件
如果你希望日志文件按日期轮转,可以直接使用 Monolog 自带的Monolog\Handler\RotatingFileHandler。修改config/autoload/logger.php,把Handler换成Monolog\Handler\RotatingFileHandler::class,并把stream字段改为filename:
<?php return [ 'default' => [ 'handler' => [ 'class' => Monolog\Handler\RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Level::Debug, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => null, 'allowInlineLineBreaks' => true, ], ], ], ];如果你需要更细粒度的日志切分,还可以继承Monolog\Handler\RotatingFileHandler并重写rotate()方法,自行控制轮转逻辑。
配置多个 Handler
用户可以通过修改handlers让同一组日志支持多个handler。例如下面的配置:当用户提交INFO及以上级别的日志时,会同时写入hyperf.log与hyperf-debug.log;当提交DEBUG级别日志时,只写入hyperf-debug.log。
方式一:在handlers数组中内联每个 Handler 的完整配置
<?php declare(strict_types=1); use Monolog\Handler; use Monolog\Formatter; use Monolog\Level; return [ 'default' => [ 'handlers' => [ [ 'class' => Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Level::Info, ], 'formatter' => [ 'class' => Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => null, 'allowInlineLineBreaks' => true, ], ], ], [ 'class' => Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf-debug.log', 'level' => Level::Info, ], 'formatter' => [ 'class' => Formatter\JsonFormatter::class, 'constructor' => [ 'batchMode' => Formatter\JsonFormatter::BATCH_MODE_JSON, 'appendNewline' => true, ], ], ], ], ], ];方式二:handlers引用其他 channel 的名称
<?php declare(strict_types=1); use Monolog\Handler; use Monolog\Formatter; use Monolog\Level; return [ 'default' => [ 'handlers' => ['single', 'daily'], ], 'single' => [ 'handler' => [ 'class' => Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Level::Info, ], ], 'formatter' => [ 'class' => Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => null, 'allowInlineLineBreaks' => true, ], ], ], 'daily' => [ 'handler' => [ 'class' => Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf-debug.log', 'level' => Level::Info, ], ], 'formatter' => [ 'class' => Formatter\JsonFormatter::class, 'constructor' => [ 'batchMode' => Formatter\JsonFormatter::BATCH_MODE_JSON, 'appendNewline' => true, ], ], ], ];方式二正是发布配置中stackchannel 的实现方式:handlers里的字符串会被LoggerFactory解析为logger.channels.<名称>下的handler与formatter配置,见 LoggerFactory.php。两种方式的结果一致,以实际写入两个文件为例:
==> runtime/logs/hyperf.log <== [2019-11-08 11:11:35] hyperf.INFO: 5dc4dce791690 [] [] ==> runtime/logs/hyperf-debug.log <== {"message":"5dc4dce791690","context":[],"level":200,"level_name":"INFO","channel":"hyperf","datetime":{"date":"2019-11-08 11:11:35.597153","timezone_type":3,"timezone":"Asia/Shanghai"},"extra":[]} {"message":"xxxx","context":[],"level":100,"level_name":"DEBUG","channel":"hyperf","datetime":{"date":"2019-11-08 11:11:35.597635","timezone_type":3,"timezone":"Asia/Shanghai"},"extra":[]}可以看到,第一个文件使用LineFormatter输出纯文本行,第二个文件使用JsonFormatter输出结构化 JSON。对应地,LoggerFactoryTest.php 中的testHandlersConfig用例正是验证了配置两个 Handler 后 Logger 实例上会挂载两个 Handler。
Request 级统一日志:自定义 Processor
有时我们需要把同一次请求产生的日志关联起来。为此可以实现一个Processor,把请求 ID 与协程 ID 注入到每条记录的extra中:
<?php declare(strict_types=1); namespace App\Kernel\Log; use Hyperf\Context\Context; use Hyperf\Coroutine\Coroutine; use Monolog\LogRecord; use Monolog\Processor\ProcessorInterface; class AppendRequestIdProcessor implements ProcessorInterface { public const REQUEST_ID = 'log.request.id'; public function __invoke(array|LogRecord $record) { $record['extra']['request_id'] = Context::getOrSet(self::REQUEST_ID, uniqid()); $record['extra']['coroutine_id'] = Coroutine::id(); return $record; } }然后在logger.php配置中注册它:
<?php declare(strict_types=1); use App\Kernel\Log; return [ 'default' => [ // 省略其他配置 'processors' => [ [ 'class' => Log\AppendRequestIdProcessor::class, ], ], ], ];这里的实现充分利用了 Hyperf 的两个特性:Hyperf\Context\Context的getOrSet保证同一次请求(上下文)内request_id恒定;Hyperf\Coroutine\Coroutine::id()记录当前协程 ID,方便在并发日志中追踪协程。这样后续在日志平台按request_id检索,就能把一次请求横跨的整条调用链日志串联起来。
从源码看,LoggerFactory的processors()方法会读取processors配置,若只有单个processor(单数)配置也会自动归一为数组,且每个元素既可以是['class' => ..., 'constructor' => ...]数组,也可以直接是闭包回调,见 LoggerFactory.php。对应的 LoggerFactoryTest.php 用processor-testchannel 验证了"类 Processor + 闭包 Processor"混配的场景。
源码级原理小结
结合前面所有配置形态,可以总结出LoggerFactory的完整装配流程(src/logger/src/LoggerFactory.php):
get($name, $channel)先从缓存$this->loggers[$channel][$name]查找,命中则直接返回,避免重复创建;- 未命中时调用
make(),读取logger.channels.<channel>配置;若该配置是闭包则先执行得到数组; handlers()解析配置:handlers数组中的元素可以是内联数组,也可以是其他 channel 的字符串名;每个 Handler 未显式指定 class 时使用默认的Monolog\Handler\StreamHandler,未指定 formatter 时使用默认LineFormatter;- 对每个 Handler,若它实现了
FormattableHandlerInterface,则用容器make()出 Formatter 并setFormatter()绑定; processors()解析并实例化所有 Processor(类或闭包);- 最终通过容器
make(Hyperf\Logger\Logger::class, ...)产出 Logger。
协程安全方面,组件做了两处适配:一是文档反复强调的"不开useLocking",因为在 Swoole 协程下文件锁会阻塞整个 Worker 进程,影响并发性能;二是通过 AOP 切面 src/logger/src/Aspect/UdpSocketAspect.php 对 Monolog 的SyslogUdp\UdpSocket::getSocket做协程化改造——在协程中为每个实例维护独立的 socket(WeakMap缓存),避免 UDP socket 在协程间共享导致的数据串扰,这是对协程环境日志安全性的进一步补充。
至此,从一行composer require到 Request 级全链路日志关联,你已经掌握了 Hyperf 日志体系的全貌。实际项目中建议按"开发环境可读、生产环境结构化、多 Handler 分流、Processor 注入公共上下文"的原则来组织config/autoload/logger.php,再配合App\Log静态门面统一业务侧调用,即可获得一套清晰、可检索、可观测的日志系统。
- 后端
- 微服务
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
相关推荐
Hyperf Logger 组件实战指南:基于 Monolog 的协程安全日志体系配置与扩展
Hyperf Logger 组件实战指南:基于 Monolog 的协程安全日志体系配置与扩展 hyperf/logger 是 Hyperf 框架的日志组件,它基
后端Web框架微服务RPC框架异步编程Hyperf 日志组件(hyperf/logger)完整指南:基于 PSR-3 与 Monolog 的协程安全日志实践
Hyperf 日志组件(hyperf/logger)完整指南:基于 PSR 3 与 Monolog 的协程安全日志实践 hyperf/logger 是 Hype
后端微服务Hyperf Logger 日志组件实战:从 Monolog 基础到协程安全的高阶配置
Hyperf Logger 日志组件实战:从 Monolog 基础到协程安全的高阶配置 hyperf/logger 是 Hyperf 协程框架的日志组件,它基于
后端Web框架微服务RPC框架异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考