PHP 怎么结构化日志

wen PHP项目 1

本文目录导读:

PHP 怎么结构化日志

  1. 使用 PSR-3 标准 + Monolog(最推荐)
  2. 原生 PHP 自定义封装(轻量级)
  3. 高级特性:利用 PHP 8 的枚举和属性(现代实践)
  4. 关键最佳实践(必读)
  5. 监控与集成(Laravel / Symfony)

在 PHP 中实现结构化日志,主要是为了将日志从纯文本(如 "User 123 logged in")转变为机器可读的格式(如 JSON),便于日志分析工具(ELK、Loki 等)进行检索和聚合。

以下是 PHP 结构化日志的几种常见实践方案,从入门到进阶:

使用 PSR-3 标准 + Monolog(最推荐)

这是目前 PHP 生态中最标准、最主流的方式。Monolog 是 PHP 的事实标准日志库,完全遵循 PSR-3 接口。

核心思路:利用 Monolog 的 Context 参数来附带结构化数据,并配置 JSON 格式的 Formatter 将日志最终输出为 JSON 字符串。

<?php
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
$log = new Logger('app');
// 创建文件处理器
$handler = new StreamHandler(__DIR__ . '/logs/app.log', Logger::INFO);
// 关键步骤:设置 JSON 格式化器
$handler->setFormatter(new JsonFormatter());
$log->pushHandler($handler);
// --- 结构化日志写入 ---
$log->info('User created', [
    'user_id' => 12345,
    'username' => 'john_doe',
    'email' => 'john@example.com',
    'source_ip' => '192.168.1.1',
    'request_id' => uniqid('req_', true) // 链路追踪 ID
]);
// 错误日志同样携带上下文
$log->error('Database connection failed', [
    'exception' => (string) $e, // 转换为字符串
    'query' => 'SELECT * FROM users'
]);

输出结果(在 app.log 中)

{"message":"User created","context":{"user_id":12345,"username":"john_doe","email":"john@example.com","source_ip":"192.168.1.1","request_id":"req_65777d9c0d1d9"},"level":200,"level_name":"INFO","channel":"app","datetime":"2023-10-27T10:00:00+00:00","extra":{}}

原生 PHP 自定义封装(轻量级)

如果项目较老,不想引入第三方依赖,可以自己写一个简单的 JSON 日志封装。

<?php
class JsonLogger
{
    private string $logFile;
    private string $channel;
    public function __construct(string $logFile = 'app.json.log', string $channel = 'app')
    {
        $this->logFile = $logFile;
        $this->channel = $channel;
    }
    public function log(string $level, string $message, array $context = []): void
    {
        // 组装结构化数据
        $logEntry = [
            'timestamp' => (new DateTimeImmutable())->format(DateTimeInterface::ATOM),
            'channel' => $this->channel,
            'level' => strtoupper($level),
            'message' => $message,
            // 额外上下文
            'context' => $context,
            // 自动附加一些系统信息
            'host' => gethostname(),
            'pid' => getmypid()
        ];
        // 以 JSON 格式追加写入
        $line = json_encode($logEntry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        file_put_contents($this->logFile, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
    public function info(string $message, array $context = []) { $this->log('info', $message, $context); }
    public function error(string $message, array $context = []) { $this->log('error', $message, $context); }
    // ... 其他级别
}
// 使用
$logger = new JsonLogger();
$logger->info('Payment processed', [
    'order_id' => 8888,
    'amount' => 100.50,
    'currency' => 'CNY'
]);

高级特性:利用 PHP 8 的枚举和属性(现代实践)

为了更规范化,可以定义日志级别的枚举和错误码属性。

<?php
enum LogLevel: string
{
    case DEBUG = 'debug';
    case INFO = 'info';
    case ERROR = 'error';
}
#[Attribute]
class LogContext
{
    public string $field;
    public string $type;
    // 构造函数定义元数据
    public function __construct(string $field, string $type = 'string') { ... }
}
class PaymentProcessor
{
    #[LogContext('order_id', 'int')]
    public int $orderId;
    // 快速记录日志函数
    public function logPayment(LogLevel $level, string $message): void
    {
        $reflection = new ReflectionClass($this);
        $context = [];
        foreach ($reflection->getProperties() as $property) {
            $attr = $property->getAttributes(LogContext::class);
            if (!empty($attr)) {
                $context[$attr[0]->newInstance()->field] = $this->{$property->getName()};
            }
        }
        // 写入 (此处可注入 Monolog)
        echo json_encode([
            'level' => $level->value,
            'message' => $message,
            'context' => $context
        ]);
    }
}
// 调用
$payment = new PaymentProcessor();
$payment->orderId = 123;
$payment->logPayment(LogLevel::INFO, 'Payment succeeded');

关键最佳实践(必读)

在结构化日志时,注意以下几点会让日志更具价值:

A. 不要将对象直接传给 Context

  • 错误做法$logger->info('data', ['user' => $userObject]); (会导致 PHP 序列化报错或输出大片无用数据)
  • 正确做法:提取关键字段转为数组
    $logger->info('User data', [
        'id' => $user->getId(),
        'name' => $user->getName(),
        'roles' => $user->getRoles() // 保证为数组或标量
    ]);

B. 加入请求 ID/追踪 ID

  • 在中间件中生成一个唯一的 request_id,并在整个请求生命周期内的所有日志中携带它,这对于排查分布式调用链至关重要。

C. 区分业务日志与异常日志

  • 异常日志必须包含 exception 信息(类名 + 堆栈),且堆栈通常需要转成字符串,避免 JSON 格式化报错。

D. 时区与格式统一

  • 统一使用 ISO 8601 格式 (Y-m-d\TH:i:sP),避免歧义。

监控与集成(Laravel / Symfony)

  • Laravel:内置 Monolog,配置 config/logging.php,将 driver 设置为 stack 组合 daily,并将 tap 设置为 JSON 格式化即可。
  • Symfony:使用 monolog.yaml 配置 formatter_typejson

对于 99% 的项目,直接使用 Monolog + JsonFormatter 是最省心、最符合 PSR 标准的做法,如果你在开发一个无第三方依赖的超轻量工具,则可以采用自己封装 JSON 数组的方式,结构化日志的核心目的不是“记录”,而是为了能被高效地“查询”和“分析”。

抱歉,评论功能暂时关闭!