news 2026/10/11 14:46:29

Hyperf 日志组件实战指南:基于 Monolog 的协程安全日志体系与多通道配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hyperf 日志组件实战指南:基于 Monolog 的协程安全日志体系与多通道配置
  • 后端
  • Web框架
  • 微服务
  • RPC框架
  • 异步编程

【免费下载链接】hyperf

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

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

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)自动完成两件事:

  1. 将Psr\Log\LoggerInterface绑定到容器工厂:LoggerInterface::class => fn ($container) => $container->get(LoggerFactory::class)->make(),这意味着你可以在任意位置直接依赖注入Psr\Log\LoggerInterface获取一个默认 Logger;
  2. 注册可发布配置:将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.

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

相关推荐

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

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

基于Web停车场管理系统毕设落地指南:JSP+Java+MySQL全流程复现与避坑

简介&#xff1a;这份资源是面向计算机专业学生与Web开发学习者的停车场管理系统毕业设计文档&#xff0c;围绕城市停车难问题&#xff0c;给出从需求分析到系统实现的完整方案。内容涵盖绪论、系统分析、系统设计与实现等章节&#xff0c;重点讲解基于Java与Spring Boot的后端…

作者头像 李华
网站建设 2026/10/11 14:36:03

安全补丁管理流程全解析:从分类评估到回退脚本的工程实践

简介&#xff1a;这份《安全补丁更新流程》文档面向系统安全管理员、运维工程师及信息安全从业者&#xff0c;用于规范操作系统、应用程序与数据库等补丁的更新管理&#xff0c;解决补丁更新不及时或操作不当引发的安全风险。资源包内含1个doc文档&#xff0c;约214KB&#xff…

作者头像 李华
网站建设 2026/10/11 14:35:50

5款能同时生成图片和视频的创作平台

日常内容创作中&#xff0c;很多人需要分开使用绘图、剪辑工具&#xff0c;频繁切换不仅效率低&#xff0c;还容易出现画面风格不统一、素材衔接生硬等问题。目前多款AI平台已实现图片视频一体化生成&#xff0c;一个工具即可完成出图、成片、创意素材制作。 本文精简实测5款实…

作者头像 李华
网站建设 2026/10/11 14:35:12

VC++界面编程:26个MFC控件实例源码深度拆解与集成指南

简介&#xff1a;面向使用 VC 进行 Windows 界面编程的开发者&#xff0c;这份实例合集围绕 26 个通用控件&#xff0c;展示按钮、编辑框、组合框、列表框、对话框、单选与复选等常见控件的实现方式&#xff0c;并结合 MFC 框架讲解控件属性、消息映射、动态创建和自定义控件等…

作者头像 李华