日志通道与驱动

日志通道(Channel)是日志系统的核心抽象,每个通道对应一个独立的日志驱动实例。通过通道可以实现日志的分类存储和路由。

通道概念

text
LogManager
├── file 通道 → File Driver → runtime/logs/
├── email 通道 → Email Driver → SMTP
└── custom 通道 → Custom Driver → ...

不同类型的日志可以路由到不同的通道,实现灵活的日志管理策略。

内置驱动:File

框架内置文件日志驱动 Viswoole\Log\Drives\File,提供完整的文件日志解决方案。

存储结构

text
runtime/logs/
├── 20240101/                    # 按日期分目录
│   ├── info/
│   │   ├── info_0.log
│   │   ├── info_1.log
│   │   └── info_2.log
│   ├── error/
│   │   ├── error_0.log
│   │   └── error_1.log
│   ├── debug/
│   └── sql/
├── 20240102/
│   └── ...

特性说明

1. 按日期分级目录

日志按 {日期}/{级别} 的目录结构组织,便于查找和管理。

2. 文件大小切割

单个日志文件超过设定大小时自动切割为新文件:

php
// 文件命名规则: {level}_{序号}.log
// info_0.log → info_1.log → info_2.log → ...

3. 自动过期清理

通过 Swoole 定时器每日凌晨自动清理超过保留天数的日志:

php
// 启动定时器(File 驱动构造函数中自动启动)
$fileDriver = new File(
    storageDays: 7,    // 保留7天
    maxFiles: 30,      // 单级最多30个文件
    fileSize: 10485760 // 单文件最大10MB
);

4. JSON 格式存储

默认以 JSON 格式存储每条日志,便于后续分析和检索:

json
{
  "timestamp": "2024-01-01T12:00:00+08:00",
  "level": "info",
  "message": "用户登录",
  "context": { "user_id": 100 },
  "source": "AuthController.php:25"
}

也可配置为纯文本格式:

php
new File(logFormat: '[%timestamp][%level]: %message %context in %source', json: false);

File 驱动完整参数

php
new \Viswoole\Log\Drives\File(
    storageDays: 7,                          // 日志保留天数
    maxFiles: 30,                            // 单级别最大文件数
    fileSize: 1024 * 1024 * 10,             // 单文件最大字节数(10MB)
    dateFormat: 'c',                         // 时间戳格式(ISO 8601)
    logFormat: '[%timestamp][%level]: %message %context in %source',
    json: true,                              // 是否JSON格式
    json_flags: JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
    log_dir: BASE_PATH . '/runtime/logs'     // 日志根目录
);

通道配置

基础配置

php
// config/log.php
return [
    'default' => 'file',
    'channels' => [
        // 字符串形式:直接指定驱动类名
        'file' => \Viswoole\Log\Drives\File::class,

        // 数组形式:带选项配置
        'daily' => [
            'driver' => \Viswoole\Log\Drives\File::class,
            'options' => [
                'storageDays' => 30,
                'maxFiles' => 100,
                'fileSize' => 20 * 1024 * 1024,  // 20MB
                'log_dir' => BASE_PATH . '/runtime/daily_logs',
            ]
        ],

        // 实例形式:直接传入驱动实例
        'custom' => new CustomLogDriver(...),
    ]
];

级别路由(type_channel)

将特定级别的日志自动路由到指定通道:

php
return [
    'default' => 'file',

    // 按级别指定通道
    'type_channel' => [
        // error 级别及以上的日志写到 error 通道
        'error' => 'error_channel',

        // 支持数组形式同时写多个通道
        'critical' => ['error_channel', 'alert_service'],

        // SQL 日志单独记录
        'sql' => 'sql_channel',
    ],

    'channels' => [
        'file' => File::class,
        'error_channel' => [
            'driver' => File::class,
            'options' => ['log_dir' => BASE_PATH . '/runtime/error_logs']
        ],
        'sql_channel' => [
            'driver' => File::class,
            'options' => ['log_dir' => BASE_PATH . '/runtime/sql_logs']
        ],
    ],
];

路由优先级:type_channel 配置 > default 默认通道

自定义驱动

实现接口

自定义日志驱动需实现 DriveInterface 接口:

php
namespace App\Log;

use Viswoole\Log\Contract\DriveInterface;
use Viswoole\Log\Collector;

class DatabaseLogDrive extends Collector implements DriveInterface
{
    public function __construct()
    {
        // 初始化数据库连接等资源
    }

    /**
     * 批量保存日志记录
     *
     * @param array<int,array{timestamp:int,level:string,message:string,context:array,source:string}> $logRecords
     */
    public function save(array $logRecords): void
    {
        foreach ($logRecords as $record) {
            // 写入数据库
            DB::table('system_logs')->insert([
                'level' => $record['level'],
                'message' => $record['message'],
                'context' => json_encode($record['context']),
                'source' => $record['source'],
                'created_at' => date('Y-m-d H:i:s', $record['timestamp']),
            ]);
        }
    }

    /**
     * 绕过缓存直接写入
     */
    public function write(string $level, string|\Stringable $message, array $context = []): void
    {
        $this->save([
            \Viswoole\Log\LogManager::createLogData($level, $message, $context)
        ]);
    }

    /**
     * 缓存日志(协程结束时会调用 save)
     */
    public function record(string $level, string|\Stringable $message, array $context = []): void
    {
        // 调用基类 Collector 的 mixed 方法进行缓存
        $this->mixed($level, $message, $context);
    }
}

继承基类

也可以继承 Viswoole\Log\Drive 抽象类,简化实现:

php
namespace App\Log;

use Viswoole\Log\Drive;

class ElasticsearchDrive extends Drive
{
    protected \Elasticsearch\Client $client;

    public function __construct(array $config)
    {
        $this->client = \Elasticsearch\ClientBuilder::create()
            ->setHosts($config['hosts'])
            ->build();
    }

    public function save(array $logRecords): void
    {
        $params = ['body' => []];

        foreach ($logRecords as $record) {
            $params['body'][] = [
                'index' => [
                    '_index' => 'app_logs',
                ]
            ];
            $params['body'][] = $record;
        }

        if (!empty($params['body'])) {
            $this->client->bulk($params);
        }
    }
}

注册自定义驱动

php
// 方式一:在配置文件中注册
// config/log.php
'channels' => [
    'elasticsearch' => [
        'driver' => \App\Log\ElasticsearchDrive::class,
        'options' => [
            'hosts' => ['http://localhost:9200']
        ]
    ]
]

// 方式二:动态注册(需在服务启动前)
Log::addChannel('database', \App\Log\DatabaseLogDrive::class);

// 方式三:传入实例
Log::addChannel('custom', new CustomLogDriver($config));

多通道实战示例

分级存储策略

php
// config/log.php
return [
    'default' => 'app',

    // 错误日志单独存储,方便监控告警
    'type_channel' => [
        'error' => 'error',
        'critical' => ['error', 'alert'],  // critical 同时写两个通道
    ],

    'channels' => [
        // 应用主日志
        'app' => [
            'driver' => File::class,
            'options' => [
                'log_dir' => BASE_PATH . '/runtime/logs/app',
                'storageDays' => 14,
                'json' => true,
            ]
        ],
        // 错误专用日志(更长保留期)
        'error' => [
            'driver' => File::class,
            'options' => [
                'log_dir' => BASE_PATH . '/runtime/logs/error',
                'storageDays' => 90,
                'json' => true,
            ]
        ],
        // 告警通道(可接入外部通知服务)
        'alert' => AlertNotificationDrive::class,
    ],
];