- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
hyperf/logger是 Hyperf 框架基于 PSR-3 规范(psr/logger)实现的日志组件,默认以monolog/monolog作为底层驱动,为常驻内存的协程服务提供了一套完整、可扩展的日志记录方案。本文将以官方文档为主线,结合仓库源码深入讲解:如何安装与配置日志通道(Channel)、如何通过LoggerFactory获取与使用Logger、如何理解 Monolog 的Handler/Formatter/Processor核心概念,以及如何落地"按日期轮转、多 Handler 分流、统一请求级日志、环境差异化格式"等实战方案,帮助你写出协程安全、生产可用的日志代码。
组件定位与设计概述
hyperf/logger组件基于psr/logger(PSR-3)接口规范实现,默认使用monolog/monolog作为驱动。在hyperf-skeleton骨架项目中,组件会默认提供一套日志配置,默认使用Monolog\Handler\StreamHandler将日志写入文件。
这里有一个与协程密切相关的前提:由于 Swoole 已经对fopen、fwrite等文件操作函数进行了协程化处理,只要不将StreamHandler的useLocking参数设置为true,日志写入就是协程安全的。也就是说,在协程环境下多个协程并发写同一日志文件时,不会因为文件锁(flock)阻塞或产生交叉错乱。
从源码结构看,该组件的核心代码位于 src/logger/src,主要包含:
LoggerFactory.php:日志工厂,负责根据配置解析并创建Logger实例;Logger.php:Hyperf 对 MonologLogger的扩展实现,同时实现了StdoutLoggerInterface;Handler/StreamHandler.php:适配协程环境的流式处理器;Aspect/UdpSocketAspect.php:对 Monolog UDP Socket 的协程化 AOP 切面;publish/logger.php:可发布到项目中的默认配置模板。
组件依赖的第三方库可在 src/logger/composer.json 中查看:要求php >= 8.2、monolog/monolog ^3.1、psr/log ^2.0 || ^3.0。
安装
在 Hyperf 项目中通过 Composer 安装:
composer require hyperf/logger安装完成后,组件通过ConfigProvider(见 src/logger/src/ConfigProvider.php)自动完成两件事:
- 将
Psr\Log\LoggerInterface绑定到容器工厂:LoggerInterface::class => fn ($container) => $container->get(LoggerFactory::class)->make(),这意味着你可以在任意位置直接依赖注入Psr\Log\LoggerInterface获取一个默认 Logger; - 注册可发布配置:将
publish/logger.php发布为config/autoload/logger.php,同时声明Hyperf\Logger\LoggerFactory的容器依赖。
配置
默认情况下,日志配置文件为项目根目录下的config/autoload/logger.php,官方文档给出的最简示例如下:
<?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, ] ], ], ];这是一个"通道(Channel)名 => 通道配置"的结构,每个通道由handler(处理器)与formatter(格式化器)两部分组成。constructor中的参数会原样传递给对应类(通过 Hyperf 容器的make()创建,支持依赖注入)。
仓库内置的默认配置模板
实际安装组件时发布出来的配置比上面的示例更完整。仓库中 src/logger/publish/logger.php 展示了组件官方推荐的多通道配置模板,其结构与上面示例略有不同(这是 3.x 版本的新式配置风格,引入了default与channels两级结构):
<?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' => env('LOG_CHANNEL', 'stack'), // 日志通道 '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], ], ], ];这个模板体现了几点值得掌握的配置能力:
default指定默认通道,可通过环境变量LOG_CHANNEL切换(如LOG_CHANNEL=daily);stack通道通过handlers引用其他通道名(用逗号分隔多个),形成"栈"式组合,默认引用single;single(单文件)、daily(按日轮转)、stderr(标准错误输出)、syslog(系统日志)、null(丢弃日志)覆盖了最常见的落地场景;- 每个通道都可以配置独立的
processors,例如stderr通道挂载了PsrLogMessageProcessor,用于将 PSR-3 风格的占位符(如{user})替换为上下文中的实际值。
新旧两种配置风格的兼容
LoggerFactory的构造函数(见 src/logger/src/LoggerFactory.php)对配置做了兼容处理:如果配置中不存在logger.channels键,或者logger.default本身是一个数组(即旧式配置,把所有通道直接挂在logger下),会自动将其转换为:
$this->config->set('logger', [ 'default' => 'default', 'channels' => $this->config->get('logger'), ]);也就是说,文档示例中的"default=> [...]"这种旧式写法(default既是默认通道名又是通道配置数组)也能正常工作,LoggerFactory会把它重写为channels.default。
可调用(Callable)配置
从源码的make()方法(src/logger/src/LoggerFactory.php)可以看到,通道配置除了可以是数组,还可以是可调用对象(闭包):
$config = $this->config->get($key, []); if (is_callable($config)) { $config = $config($name); }闭包会接收到日志的$name参数,据此可以动态生成与 name 绑定的配置。这一特性在 src/logger/tests/LoggerFactoryTest.php 的testInvokeLoggerByCallableConfigFromFactory用例中有直接验证:测试中通过闭包配置把日志流指向BASE_PATH . '/runtime/logs/' . $name . '.log',使每个 Logger name 对应独立日志文件。
使用
在业务类中,通过依赖注入LoggerFactory获取指定 name 与通道的Logger实例:
<?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) { // 第一个参数对应日志的 name, 第二个参数对应 config/autoload/logger.php 内的 key $this->logger = $loggerFactory->get('log', 'default'); } public function method() { // Do something. $this->logger->info("Your log message."); } }参数说明:
- 第一个参数
$name:日志名,即 Monolog 中的 Channel 名,会出现在日志记录里用于区分来源; - 第二个参数
$channel:对应config/autoload/logger.php中的通道 key(即logger.channels.<channel>),省略时使用default指定的默认通道。
从LoggerFactory::get()的实现(src/logger/src/LoggerFactory.php)可以看到,同一name+channel组合的Logger会被缓存在$loggers[$channel][$name]中,重复获取返回同一个实例,避免了每次调用都重建 Handler 与文件流:
public function get(string $name = 'hyperf', ?string $channel = null): LoggerInterface { $channel = $channel ?? $this->config->get('logger.default', 'default'); if (isset($this->loggers[$channel][$name]) && $this->loggers[$channel][$name] instanceof Logger) { return $this->loggers[$channel][$name]; } return $this->loggers[$channel][$name] = $this->make($name, $channel); }Hyperf 的Logger类(src/logger/src/Logger.php)继承自Monolog\Logger,同时实现了Hyperf\Contract\StdoutLoggerInterface,并在构造时调用了useLoggingLoopDetection(false)关闭 Monolog 的日志循环检测,以规避协程并发场景下的误判。你拿到的实例天然兼容 PSR-3 的emergency / alert / critical / error / warning / notice / info / debug全套日志级别方法。
注意:默认容器里
Psr\Log\LoggerInterface的绑定(来自 ConfigProvider)等价于LoggerFactory::make(),即使用默认通道与默认 name,可直接构造注入使用。
关于 Monolog 的基础知识
要熟练使用 Hyperf 日志组件,先要理解 Monolog 的核心概念。官方文档用一段代码完整演示了Logger、Handler、Formatter、Processor的协作关系:
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); // clone new log channel $log2 = $log->withName('log2'); // add records to the log $log->warning('Foo'); // add extra data to record // 1. log context $log->error('a 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');结合这段代码,可以梳理出 Monolog 的核心工作流:
Logger与 Channel:实例化一个Logger并命名,名字即channel,用于标识日志来源(如hyperf、app);Handler处理日志:一个Logger可以绑定多个Handler,打日志时由Logger依次交给所有Handler处理(推入 Handler 队列);- 级别过滤:
Handler可以指定自己处理哪些日志级别,例如new StreamHandler('test.log', Logger::WARNING)表示只处理级别>= WARNING的日志,更低的INFO/DEBUG会被该 Handler 忽略; Formatter格式化:日志文本由Formatter负责排版,先创建 Formatter 再通过setFormatter()绑定到对应的Handler上;- 日志记录的结构:一条日志通常包含
datetime、channel、level_name、message、context、extra等部分,可通过格式化字符串定制,如"%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n"; context与extra的区别:context由调用者打日志时额外传入(更灵活,随条日志变化,如['username' => 'daydaygo']);extra由绑定在Logger上的Processor固定追加(适合收集一些通用信息,如内存占用、请求 ID)。
在 Hyperf 组件中,这套机制被LoggerFactory的handlers()/processors()方法(src/logger/src/LoggerFactory.php)自动化了:它读取配置中的handlers(或单个handler)与processors,通过容器make()创建 Handler 与 Formatter 实例,并自动执行$handler->setFormatter($formatter)(仅当 Handler 实现了FormattableHandlerInterface)。同时它内置了默认值:未配置 handler 时默认使用StreamHandler+BASE_PATH/runtime/logs/hyperf.log+DEBUG级别(见getDefaultHandlerConfig()),未配置 formatter 时默认使用LineFormatter(见getDefaultFormatterConfig())。
更多用法
封装Log类
如果你更习惯"Log::info()"这种多数框架的静态调用方式,可以在App命名空间下创建一个Log门面类,通过容器获取LoggerFactory并转发调用:
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); } }默认使用 Channel 名为app的 Logger 记录日志,你也可以通过Log::get($name)获取其他 Channel 的Logger。由于Logger本身继承自Monolog\Logger,直接调用Log::get('app')->info(...)即可记录各级别日志;容器(Container)负责了实例的获取与复用。
⚠️重要提醒(官方文档明确警告):在使用时,不要让
$name与请求挂钩,比如把$request_id当作 Logger name 使用。由于LoggerFactory会缓存$loggers[$channel][$name],这会导致请求级别的日志对象被永久保存在 Factory 中,引发严重的内存泄漏。正确的做法是让$name只代表固定的业务模块(如app、sys、order),而把请求级信息通过context或Processor写入日志内容。
stdout 日志
框架组件(如服务启动、请求分发等)默认输出的日志,由Hyperf\Contract\StdoutLoggerInterface接口的实现类Hyperf\Framework\Logger\StdoutLogger提供支持。该实现类的源码位于 src/framework/src/Logger/StdoutLogger.php,它通过Symfony\Component\Console\Output\ConsoleOutput将格式化后的信息输出到标准输出(stdout),即启动 Hyperf 的终端(Terminal)上,因此它并没有使用 Monolog。
StdoutLogger还支持按级别过滤:它读取config/autoload/logger.php中Hyperf\Contract\StdoutLoggerInterface::class => ['log_level' => [...]]的配置(源码 src/framework/src/Logger/StdoutLogger.php),只有log_level数组中列出的级别才会被输出。其默认绑定关系在 src/framework/src/ConfigProvider.php 中声明为StdoutLoggerInterface::class => StdoutLogger::class。
如果你希望 stdout 日志也统一走 Monolog(保持格式一致、便于收集),仍然可以通过强大的容器(Container)替换默认实现:
- 首先,实现一个
StdoutLoggerFactory类(关于 Factory 的用法可在 依赖注入 章节获得更多详细说明):
<?php declare(strict_types=1); namespace App; use Psr\Container\ContainerInterface; class StdoutLoggerFactory { public function __invoke(ContainerInterface $container) { return Log::get('sys'); } }- 然后声明依赖,让所有依赖
StdoutLoggerInterface的地方,都使用StdoutLoggerFactory实例化的类:
// config/autoload/dependencies.php return [ \Hyperf\Contract\StdoutLoggerInterface::class => \App\StdoutLoggerFactory::class, ];之后框架层面的 stdout 日志就与业务日志一样,统一由配置了sys通道(推荐使用php://stdout流)的 Monolog Logger 输出。
不同环境下输出不同格式的日志
Handler与Formatter可以按运行环境灵活组合。例如,在开发(dev)环境输出人类可读的多行文本,在生产环境输出 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,底层会自动把default这一Handler绑定到Logger上(对应LoggerFactory::handlers()中$handlerConfigs = $config['handlers'] ?? [[]]的兜底逻辑,未显式配置时使用默认 Handler 配置); - dev 环境:日志通过
php://stdout输出到标准输出,且Formatter设置allowInlineLineBreaks,方便查看多行日志,同时开启includeStacktraces输出堆栈; - 非 dev 环境:日志使用
JsonFormatter,被格式化为 JSON,方便结构化采集与投递到第三方日志服务。
日志文件按日期轮转
如果希望日志文件按日期轮转,可直接使用 Monolog 已提供的Monolog\Handler\RotatingFileHandler。修改config/autoload/logger.php,将Handler改为RotatingFileHandler,并把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, ], ], ], ];RotatingFileHandler会按天生成带日期的日志文件(如hyperf.log-2026-10-11),并自动清理过期文件。仓库内置模板中的daily通道(src/logger/publish/logger.php)正是这种配置。如果你希望进行更细粒度的日志切割,也可以继承Monolog\Handler\RotatingFileHandler类并重新实现rotate()方法,实现自定义的切割策略(如按小时、按大小)。
配置多个Handler
一个日志组可以配置多个Handler,实现"同一日志按级别分流到不同文件"等效果。官方文档给出了两种写法。
写法一:在handlers内联多个 Handler 配置。例如下面配置中,当用户投递一个INFO级别及以上的日志时,会同时写入hyperf.log和hyperf-debug.log;当投递DEBUG级别日志时,只会写入hyperf-debug.log(因为hyperf.log对应的 Handler 级别是INFO):
<?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内的元素也可以是字符串,此时它表示logger.channels下的另一个通道名,LoggerFactory会读取该通道的handler与formatter配置(见 src/logger/src/LoggerFactory.php):
<?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, ], ], ], ];两种写法对应的输出效果如下(hyperf.log使用LineFormatter输出纯文本行,hyperf-debug.log使用JsonFormatter输出 JSON):
==> 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":[]}从 JSON 记录可以看出:level字段是 Monolog 内部的级别数值(INFO为 200、DEBUG为 100),channel记录了 Logger 的 name,datetime记录了完整时间与时区信息。
统一请求级别日志
在微服务与协程场景下,我们经常需要把"同一个请求"产生的所有日志关联起来。可以通过实现一个自定义Processor,利用 Hyperf 的协程上下文(Hyperf\Context\Context)与协程 ID(Hyperf\Coroutine\Coroutine)为每条日志追加request_id与coroutine_id:
<?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; } }然后将该 Processor 配置到logger.php的processors中:
<?php declare(strict_types=1); use App\Kernel\Log; return [ 'default' => [ // 删除其他配置 'processors' => [ [ 'class' => Log\AppendRequestIdProcessor::class, ], ], ], ];实现要点说明:
Context::getOrSet()会在协程上下文中缓存request_id,保证同一请求内(包括跨协程传递的上下文)日志携带相同的request_id,方便按请求链路聚合检索;Coroutine::id()记录当前协程 ID,便于排查并发问题;- 配置项
processors支持"类 + 可选 constructor 参数"的形式(LoggerFactory::processors()会通过make($value['class'], $value['constructor'] ?? [])创建实例),同时也支持直接写闭包(见 src/logger/tests/LoggerFactoryTest.php 的testProcessor用例,其中混合使用了两个类 Processor 与一个闭包 Processor,验证了三种写法均可生效); - 配置兼容
processor(单数)与processors(复数)两种写法,未设置processors而设置了processor时会被自动包装为数组(src/logger/src/LoggerFactory.php)。
协程安全:深入 StreamHandler 与 UDP Socket
官方文档特别强调"只要不将useLocking参数设置为true,就是协程安全的",其依据可以从 Hyperf 的StreamHandler源码(src/logger/src/Handler/StreamHandler.php)中看出端倪:
- 构造参数
useLocking默认为false,此时write()方法直接通过fwrite()写入流,不执行flock()加锁(源码第 176-200 行);而 Swoole 已将fopen/fwrite等函数协程化,I/O 期间会自动让出协程调度,因此并发写入是安全的; - 当
useLocking = true时,write()会先flock($stream, LOCK_EX)再写入、写入后flock($stream, LOCK_UN),虽然保证了跨进程/跨协程的强一致性,但会引入阻塞开销,因此默认不开启; - 该 Handler 还针对长驻进程做了增强:每次
write()前通过 inode 比对检测日志文件是否被外部轮转(hasUrlInodeWasChanged()),若 inode 发生变化(如被logrotate切割)则自动重开文件流,避免写入已删除的文件;写入失败时还会自动关闭并重试一次(retrying机制); - 写入前会自动创建目录(
createDir()使用mkdir(..., 0777, true)),并且 chunk size 会根据 PHPmemory_limit动态调整(默认最大 10MB 分块),避免大日志记录撑爆内存。
此外,Monolog 的SyslogUdp\UdpSocket在协程环境下会共享底层 socket 资源,Hyperf 通过 AOP 切面 src/logger/src/Aspect/UdpSocketAspect.php 对其getSocket()方法进行拦截:在协程内为每个协程维护独立的 socket(存于WeakMap),协程外仍使用非协程 socket,从而避免 socket 句柄在协程间串用导致的异常。
测试验证
该组件的核心行为均有单元测试覆盖,可作为理解与验证实现细节的参考:
- src/logger/tests/LoggerFactoryTest.php:验证
LoggerFactory的实例化、get()获取 Logger、可调用(闭包)配置、字符串引用的多 Handler、handlers数组多 Handler、processors/processor配置的解析与执行(包括类实例与闭包两种形式); - src/logger/tests/LoggerTest.php:验证
Hyperf\Logger\Logger同时是Monolog\Logger、StdoutLoggerInterface、Psr\Log\LoggerInterface的实例;支持直接记录Throwable对象;并通过parallel()并行写入验证关闭日志循环检测后在协程并发下不会产生重复日志; - src/framework/tests/StdoutLoggerTest.php:验证
StdoutLogger的log_level配置过滤行为。
总结
hyperf/logger组件以 PSR-3 规范为契约、Monolog 为引擎,通过LoggerFactory将"配置即通道、通道即 Handler 组合"的理念落地:你可以用一套配置文件管理单文件、按日轮转、stdout/stderr、syslog、null 等多种输出,自由组合多个 Handler 实现日志分级分流,借助 Formatter 实现开发/生产环境的差异化格式,通过 Processor 为日志统一注入请求 ID 与协程 ID。在协程层面,组件通过协程安全的StreamHandler与 UDP Socket 切面,保证了高并发长驻进程下的写入可靠性与资源隔离。
无论你是刚接触 Hyperf 的新手,还是需要精细化治理日志的老手,都可以从config/autoload/logger.php出发,按本文提供的配置模板与源码依据逐步定制属于自己的日志方案。
- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
相关推荐
Hyperf 日志组件实战指南:基于 Monolog 的协程安全日志体系与多 Handler 配置
Hyperf 日志组件实战指南:基于 Monolog 的协程安全日志体系与多 Handler 配置 本篇技术指南围绕 Hyperf 框架的 hyperf/log
后端微服务Hyperf 日志组件实战指南:基于 Monolog 构建协程安全的日志体系
Hyperf 日志组件实战指南:基于 Monolog 构建协程安全的日志体系 导读 本文以 Hyperf 官方文档中「日誌」章节( docs/zh tw/log
后端微服务Hyperf Logger 组件实战指南:基于 Monolog 的协程安全日志体系配置与扩展
Hyperf Logger 组件实战指南:基于 Monolog 的协程安全日志体系配置与扩展 hyperf/logger 是 Hyperf 框架的日志组件,它基
后端Web框架微服务RPC框架异步编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考