日志使用指南
本篇介绍通过 Log 门面记录日志的日常用法:级别方法、上下文数据、协程聚合写入机制与多通道操作。级别语义与通道路由规则见 级别与通道,配置项详见 配置详解。
记录日志
级别快捷方法
Viswoole\Log\Facade\Log 为每个内置级别提供同名静态方法,第二个参数为可选的上下文数组:
use Viswoole\Log\Facade\Log;
Log::info('用户登录', ['uid' => 1, 'way' => 'password']);
Log::error('库存扣减失败', ['sku' => 'A001', 'stock' => 0]);
Log::warning('接口响应时间过长', ['uri' => '/api/orders', 'duration' => '5.2s']);
Log::debug('处理前数据', ['input' => $raw]);
Log::alert('数据库不可用', ['dsn' => 'mysql:host=127.0.0.1']);
Log::sql('SELECT 查询完成', ['sql' => $sql, 'time' => '2.3ms']);
Log::task('任务执行完成', ['topic' => 'email.send', 'duration' => '0.5s']);各方法仅级别名不同,签名一致:方法名(string|Stringable $message, array $context = []): void。级别语义与使用场景见 级别与通道。
自定义级别
PSR-3 中的 emergency、critical、notice 及业务自定义级别没有独立方法,统一通过 mixed() 记录,第一个参数是级别名:
// 系统不可用
Log::mixed('emergency', '数据库连接池耗尽', ['pool_size' => 64]);
// 记录任何业务自定义级别
Log::mixed('audit', '删除了角色', ['role_id' => 3]);消息与上下文
$message支持字符串或实现了Stringable接口的对象(写入时自动转字符串)$context建议传结构化数组,File 驱动以 JSON 存储时可直接检索
Log::info(new class implements Stringable {
public function __toString(): string
{
return '对象形式的消息';
}
}, ['key' => 'value']);协程聚合写入
这是框架在协程环境下的默认行为:日志先写入当前协程私有的记录器(Viswoole\Log\Recorder),协程结束时随记录器析构一次性批量调用驱动的 save() 持久化。
public function demo(): void
{
// 以下 3 条日志此刻只进入协程缓存,并未写盘
Log::info('步骤 1 完成');
Log::debug('中间变量', ['x' => 10]);
Log::info('步骤 2 完成');
// 方法返回(协程结束)后,3 条日志合并为一次批量写入
}带来的收益与注意点:
- 一条请求产生几十条日志,也只发生一次 IO,显著降低磁盘压力
error()、info()等所有级别快捷方法都走聚合写入;聚合意味着崩溃瞬间最后一批日志可能丢失- 对必须立即落盘的日志(如支付回调、安全审计),使用
write()直写:
// 绕过协程缓存,立即调用驱动 save() 写入
Log::write('error', '支付回调验签失败,需立即持久化', ['order_id' => $orderId]);手动管理协程缓存
$pending = Log::getRecord(); // 获取当前协程尚未写入的日志
Log::clearRecord(); // 丢弃当前协程缓存的日志(不写入)
Log::save($pending); // 手动批量持久化(一般无需手动调用)非协程环境
record()(级别快捷方法底层调用)在非协程环境会自动退化为直写,无需按环境区分代码。
通道操作
按名称取通道
Log::channel() 返回指定通道的驱动实例,可直接调用该驱动的全部方法;未命中已注册通道时抛出异常:
use Viswoole\Log\Facade\Log;
Log::channel('file')->info('明确写入 file 通道');
// 判断通道是否已注册
if (Log::hasChannel('email')) {
Log::channel('email')->alert('发送告警');
}动态注册通道
addChannel() 支持驱动实例、驱动类名、['driver' => ..., 'options' => [...]] 配置数组三种形式:
use Viswoole\Log\Drives\File;
Log::addChannel('error', [
'driver' => File::class,
'options' => [
'log_dir' => BASE_PATH . '/runtime/error_logs',
'storageDays' => 90,
],
]);必须在服务启动前注册
addChannel() 需在 Swoole 服务启动前调用(如监听 AppInitialized 事件或在服务提供者中注册),工作进程运行中添加的通道不会同步到其他进程。
通常无需手动注册——config/log.php 的 channels 配置会在启动时统一注册;配合 type_channel 可实现「error 级别单独入独立通道」,见 级别与通道。
调用来源
log.trace_source 开启时(默认配置已开启),框架通过回溯调用栈自动把调用位置(文件:行号)注入日志的 source 字段:
// AuthController.php 第 25 行调用
Log::info('用户登录', ['uid' => 1]);{
"timestamp": "2026-01-01T12:00:00+08:00",
"level": "info",
"message": "用户登录",
"context": { "uid": 1 },
"source": "/app/Controller/AuthController.php:25"
}关闭时 source 记录为 not record。高频日志场景若在意 debug_backtrace 开销,可在配置中关闭。
实战示例
请求日志中间件
use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\HttpServer\Facade\Request;
use Viswoole\Log\Facade\Log;
class RequestLogMiddleware implements MiddlewareInterface
{
public function process(Closure $handler): mixed
{
$start = microtime(true);
$method = Request::getMethod();
$uri = Request::getPath();
$ip = Request::ip();
$response = $handler();
// 无需手动 flush:本协程结束时,info 与下方日志会一起批量写入
Log::info('HTTP 请求', [
'method' => $method,
'uri' => $uri,
'ip' => $ip,
'duration' => round((microtime(true) - $start) * 1000, 2) . 'ms',
]);
return $response;
}
}中间件的注册方式见 中间件。
异常记录
try {
$this->refund($order);
} catch (\Throwable $e) {
Log::error('退款失败', [
'order_id' => $order['id'],
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
]);
throw $e;
}