本文目录导读:

- 使用 PSR-3 标准 + Monolog(最推荐)
- 原生 PHP 自定义封装(轻量级)
- 高级特性:利用 PHP 8 的枚举和属性(现代实践)
- 关键最佳实践(必读)
- 监控与集成(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_type为json。
对于 99% 的项目,直接使用 Monolog + JsonFormatter 是最省心、最符合 PSR 标准的做法,如果你在开发一个无第三方依赖的超轻量工具,则可以采用自己封装 JSON 数组的方式,结构化日志的核心目的不是“记录”,而是为了能被高效地“查询”和“分析”。