news 2026/10/9 5:04:18

Hyperf Logger 组件实战指南:基于 Monolog 的协程安全日志体系与高级用法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hyperf Logger 组件实战指南:基于 Monolog 的协程安全日志体系与高级用法
  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

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 的封装非常薄,核心只做了三件事:

  1. 提供一个继承自Monolog\Logger的Hyperf\Logger\Logger类,同时实现Hyperf\Contract\StdoutLoggerInterface,见 src/logger/src/Logger.php,并在构造时关闭了 Monolog 的循环日志检测(useLoggingLoopDetection(false));
  2. 提供LoggerFactory工厂,负责从配置中心读取logger配置并装配出带 Handler、Formatter、Processor 的 Logger 实例,见 src/logger/src/LoggerFactory.php;
  3. 通过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):

  1. get($name, $channel)先从缓存$this->loggers[$channel][$name]查找,命中则直接返回,避免重复创建;
  2. 未命中时调用make(),读取logger.channels.<channel>配置;若该配置是闭包则先执行得到数组;
  3. handlers()解析配置:handlers数组中的元素可以是内联数组,也可以是其他 channel 的字符串名;每个 Handler 未显式指定 class 时使用默认的Monolog\Handler\StreamHandler,未指定 formatter 时使用默认LineFormatter;
  4. 对每个 Handler,若它实现了FormattableHandlerInterface,则用容器make()出 Formatter 并setFormatter()绑定;
  5. processors()解析并实例化所有 Processor(类或闭包);
  6. 最终通过容器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.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

相关推荐

上一篇:如何永久保存微信聊天记录:WeChatMsg开源工具终极指南
下一篇:如何让微信聊天记录成为你的数字记忆宝库:WeChatMsg完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 5:01:30

基于UDP的聊天程序课程设计:C/S架构与套接字编程实战

简介&#xff1a;这份计算机网络课程设计报告面向高校计算机相关专业学生&#xff0c;聚焦基于UDP协议的局域网聊天程序开发&#xff0c;帮助读者完成从协议原理到编码实现的完整课程设计任务。报告以Visual C 6.0为开发环境&#xff0c;采用C/S模式&#xff0c;系统讲解UDP无连…

作者头像 李华
网站建设 2026/10/9 5:00:05

RPCS3 PS3 模拟器:从下载到画面稳定的 20 分钟路线

RPCS3 PS3 模拟器&#xff1a;从下载到画面稳定的 20 分钟路线 【免费下载链接】rpcs3 PlayStation 3 emulator and debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 RPCS3 是免费开源的 PS3 模拟器&#xff0c;让你在电脑上玩 PS3 游戏&#xff0c;不…

作者头像 李华
网站建设 2026/10/9 4:59:09

CPU内部工作原理:数据通路、控制器与流水线全解析

1. 一块CPU芯片里到底装了什么&#xff1a;第五章的全局脉络我记得自己当年学“计算机组成原理”第五章时&#xff0c;最大的困惑就是&#xff1a;明明叫“中央处理器”&#xff0c;为什么教材里一会儿讲电路连线、一会儿讲微指令、一会儿又讲中断&#xff0c;感觉像是三四门课…

作者头像 李华
网站建设 2026/10/9 4:58:45

C#配置文件统一管理:用PowerConfig告别杂乱配置读写与维护噩梦

接手一个维护了三年的老项目是什么体验&#xff1f;别的先不说&#xff0c;光是配置文件那一坨代码就够让人头大的。App.config里躺着十几个自造的键值对&#xff0c;另一个模块用JSON反序列化&#xff0c;还有一个模块干脆自己写了个INI解析器&#xff0c;而且每个模块读配置的…

作者头像 李华