资讯动态

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

发布时间:2026/10/9 5:04:27 来源:尧图企业网站定制
后端微服务【免费下载链接】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 的封装非常薄核心只做了三件事提供一个继承自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 可以看到实际发布的默认配置比上述示例更完整它采用defaultchannels的结构默认 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_types1); 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配置重写为defaultchannels的新结构见 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记录日志时会把处理工作委托给这些HandlerHandler可以指定处理哪些日志级别例如Logger::WARNING只处理 Logger::WARNING的日志负责格式化日志的是Formatter把Formatter设置并绑定到对应的Handler上一条日志由%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n组成注意区分日志中追加的context与extracontext是用户记录日志时自行传入的附加数据更灵活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_types1); 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_types1); 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_types1); 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_types1); 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_types1); 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.phpget($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做协程化改造——在协程中为每个实例维护独立的 socketWeakMap缓存避免 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点击查看免费下载相关推荐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框架异步编程上一篇如何永久保存微信聊天记录WeChatMsg开源工具终极指南下一篇如何让微信聊天记录成为你的数字记忆宝库WeChatMsg完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑