日志使用指南

本篇介绍通过 Log 门面记录日志的日常用法:级别方法、上下文数据、协程聚合写入机制与多通道操作。级别语义与通道路由规则见 级别与通道,配置项详见 配置详解

记录日志

级别快捷方法

Viswoole\Log\Facade\Log 为每个内置级别提供同名静态方法,第二个参数为可选的上下文数组:

php
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 中的 emergencycriticalnotice 及业务自定义级别没有独立方法,统一通过 mixed() 记录,第一个参数是级别名:

php
// 系统不可用
Log::mixed('emergency', '数据库连接池耗尽', ['pool_size' => 64]);
// 记录任何业务自定义级别
Log::mixed('audit', '删除了角色', ['role_id' => 3]);

消息与上下文

  • $message 支持字符串或实现了 Stringable 接口的对象(写入时自动转字符串)
  • $context 建议传结构化数组,File 驱动以 JSON 存储时可直接检索
php
Log::info(new class implements Stringable {
  public function __toString(): string
  {
    return '对象形式的消息';
  }
}, ['key' => 'value']);

协程聚合写入

这是框架在协程环境下的默认行为:日志先写入当前协程私有的记录器(Viswoole\Log\Recorder),协程结束时随记录器析构一次性批量调用驱动的 save() 持久化。

php
public function demo(): void
{
    // 以下 3 条日志此刻只进入协程缓存,并未写盘
    Log::info('步骤 1 完成');
    Log::debug('中间变量', ['x' => 10]);
    Log::info('步骤 2 完成');

    // 方法返回(协程结束)后,3 条日志合并为一次批量写入
}

带来的收益与注意点:

  • 一条请求产生几十条日志,也只发生一次 IO,显著降低磁盘压力
  • error()info() 等所有级别快捷方法都走聚合写入;聚合意味着崩溃瞬间最后一批日志可能丢失
  • 对必须立即落盘的日志(如支付回调、安全审计),使用 write() 直写:
php
// 绕过协程缓存,立即调用驱动 save() 写入
Log::write('error', '支付回调验签失败,需立即持久化', ['order_id' => $orderId]);

手动管理协程缓存

php
$pending = Log::getRecord();  // 获取当前协程尚未写入的日志
Log::clearRecord();           // 丢弃当前协程缓存的日志(不写入)
Log::save($pending);          // 手动批量持久化(一般无需手动调用)

非协程环境

record()(级别快捷方法底层调用)在非协程环境会自动退化为直写,无需按环境区分代码。

通道操作

按名称取通道

Log::channel() 返回指定通道的驱动实例,可直接调用该驱动的全部方法;未命中已注册通道时抛出异常:

php
use Viswoole\Log\Facade\Log;

Log::channel('file')->info('明确写入 file 通道');

// 判断通道是否已注册
if (Log::hasChannel('email')) {
    Log::channel('email')->alert('发送告警');
}

动态注册通道

addChannel() 支持驱动实例、驱动类名、['driver' => ..., 'options' => [...]] 配置数组三种形式:

php
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.phpchannels 配置会在启动时统一注册;配合 type_channel 可实现「error 级别单独入独立通道」,见 级别与通道

调用来源

log.trace_source 开启时(默认配置已开启),框架通过回溯调用栈自动把调用位置(文件:行号)注入日志的 source 字段:

php
// AuthController.php 第 25 行调用
Log::info('用户登录', ['uid' => 1]);
json
{
  "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 开销,可在配置中关闭。

实战示例

请求日志中间件

php
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;
  }
}

中间件的注册方式见 中间件

异常记录

php
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;
}

下一步