本文目录导读:

为PHP项目实现全服务统一的日志规范,核心思路是制定标准 → 集成中间件 → 统一输出,下面提供一套从定义到落地的完整方案。
统一日志格式标准(推荐 JSON 结构)
推荐使用 JSON 格式,便于日志中心(如 ELK、Loki)解析。
{
"timestamp": "2025-01-01T12:00:00.000Z",
"level": "info",
"trace_id": "a1b2c3d4-...",
"service": "user-service",
"environment": "production",
"request": {
"method": "POST",
"path": "/api/users",
"status": 201,
"duration_ms": 125
},
"message": "用户创建成功",
"context": {
"user_id": 12345,
"ip": "10.0.0.1"
},
"exception": null
}
必含字段说明
| 字段 | 说明 | 示例 |
|---|---|---|
timestamp |
ISO8601 时间(UTC) | 2025-01-01T12:00:00.000Z |
level |
日志级别 | debug, info, warning, error |
trace_id |
链路追踪 ID | 自动生成或从请求头提取 |
service |
服务名(环境变量注入) | order-service |
environment |
环境标识 | dev / staging / production |
message |
人类可读描述 | “数据库连接超时” |
context |
业务关键参数 | 用户ID、订单号、请求来源 |
exception |
异常信息(仅 error 级别) | {"class":"RuntimeException","message":"...","trace":"..."} |
PHP 侧统一实现方案
使用 Monolog + 自定义处理器(推荐)
<?php
// 全局日志配置类
class LogConfig
{
public static function createLogger(string $serviceName): Logger
{
$logger = new Logger($serviceName);
// JSON 格式化器
$formatter = new JsonFormatter();
// 写入文件(按天滚动)
$stream = new StreamHandler(
sprintf('/var/log/%s/%s.log', $serviceName, date('Y-m-d')),
Level::Debug
);
$stream->setFormatter($formatter);
// 添加 trace_id 处理器(自动注入)
$processor = new IntrospectionProcessor();
$processor->pushProcessor(function (LogRecord $record) {
$record->extra['trace_id'] = ContextHolder::getTraceId();
$record->extra['environment'] = $_ENV['APP_ENV'] ?? 'dev';
return $record;
});
$logger->pushHandler($stream);
$logger->pushProcessor($processor);
return $logger;
}
}
// 链路追踪 ID 上下文持有者(使用协程安全存储)
class ContextHolder
{
private static array $context = [];
public static function setTraceId(string $id): void
{
self::$context[getmypid()] = ['trace_id' => $id];
}
public static function getTraceId(): string
{
return self::$context[getmypid()]['trace_id'] ?? 'no-trace';
}
}
全局上下文注入(中间件模式)
Laravel 示例:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Ramsey\Uuid\Uuid;
class LogContextMiddleware
{
public function handle(Request $request, Closure $next)
{
// 从请求头或生成新 trace_id
$traceId = $request->header('X-Trace-Id') ?? Uuid::uuid4()->toString();
ContextHolder::setTraceId($traceId);
// 记录请求开始日志(可选)
Log::info('request_start', [
'method' => $request->method(),
'path' => $request->path()
]);
$response = $next($request);
return $response;
}
}
ThinkPHP 示例(使用中间件):
<?php
namespace app\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
class LogTraceMiddleware
{
public function process(Request $request, callable $handler): Response
{
$traceId = $request->header('X-Trace-Id') ?? bin2hex(random_bytes(16));
ContextHolder::setTraceId($traceId);
$request->traceId = $traceId;
return $handler($request);
}
}
日志调用封装(避免直接写 print/log 原始函数)
<?php
// 全局日志门面(Facade)
class Log
{
private static ?Logger $logger = null;
public static function info(string $message, array $context = []): void
{
self::getLogger()->info($message, $context);
}
public static function error(string $message, array $context = [], ?\Throwable $e = null): void
{
$data = $context;
if ($e) {
$data['exception'] = [
'class' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
];
}
self::getLogger()->error($message, $data);
}
private static function getLogger(): Logger
{
if (!self::$logger) {
self::$logger = LogConfig::createLogger(
$_ENV['SERVICE_NAME'] ?? 'unknown'
);
}
return self::$logger;
}
}
// 使用方式
Log::info('支付成功', ['order_id' => 123, 'amount' => 99.8]);
Log::error('支付失败', ['order_id' => 456], $exception);
统一输出目标(日志采集与传输)
方案 A:直接写磁盘 + 日志收集器(推荐)
/var/log/{service_name}/{yyyy-mm-dd}.log
- 使用 Filebeat / Fluentd 采集
- 配置示例(Filebeat):
filebeat.inputs:
- type: log
paths:
- /var/log/.log json.keys_under_root: true json.add_error_key: true
方案 B:写入 Elasticsearch 或 Loki
// Monolog Elasticsearch 处理器
$handler = new ElasticsearchHandler(
new Client(['hosts' => ['http://elastic:9200']]),
[
'index' => 'php-logs-{environment}-{date}',
'type' => '_doc'
]
);
方案 C:通过 Syslog / UDP 发送(高性能场景)
$handler = new SocketHandler('udp://logserver:514');
多服务统一保证的要点
| 措施 | 实现方式 |
|---|---|
| 格式标准化 | 使用单个依赖包(如 Monolog)统一格式 |
| 时间统一 | 全部使用 UTC 时间,服务内部转换时区显示 |
| 服务名统一 | 从环境变量 SERVICE_NAME 读取,不得硬编码 |
| trace_id 传播 | HTTP 头 X-Trace-Id,RPC 框架自动传递 |
| 环境标识 | 从 APP_ENV 注入,禁止代码判断环境 |
| 日志级别规范 | ERROR=异常/失败 WARNING=非正常但可恢复 INFO/DEBUG=正常流程 |
| 敏感信息脱敏 | context 中密码/token 使用 替换 |
禁止行为与最佳实践
- ✅ 推荐: 使用结构化 JSON 日志
- ✅ 推荐: 所有日志调用走统一门面(如上面
Log::info()) - ❌ 禁止: 直接调用
error_log()、print_r() - ❌ 禁止: 在日志中包含敏感数据(密码、完整信用卡号)
- ✅ 推荐: 异常日志必须包含
trace_id和stack trace - ❌ 禁止: 日志中混用多种时间格式(如
now()、time()) - ✅ 推荐: 使用
.env文件统一管控日志级别与环境标识
完整示例:一次请求的日志输出
# INFO 级别
{"timestamp":"2025-01-01T12:00:00.000Z","level":"info","trace_id":"abc123","service":"order-service","environment":"production","message":"request_start","context":{"method":"POST","path":"/api/orders"}}
# 业务成功
{"timestamp":"2025-01-01T12:00:01.200Z","level":"info","trace_id":"abc123","service":"order-service","environment":"production","message":"订单创建完成","context":{"order_id":789}}
# ERROR 级别
{"timestamp":"2025-01-01T12:00:02.150Z","level":"error","trace_id":"abc123","service":"order-service","environment":"production","message":"数据库写入失败","context":{"table":"orders","sql":"INSERT INTO ..."},"exception":{"class":"PDOException","message":"SQLSTATE[40001]","trace":"#0 /var/www/..."}}
总结操作步骤
- 抽离日志配置类(像上面的
LogConfig),打包为公司内部 Composer 包 - 所有服务统一引用该包
- 中间件统一注入 trace_id + 环境 + 服务名
- 所有日志调用使用
Log::info()/Log::error()门面 - 日志收集端统一采集 JSON 字段,建立 ELK/Loki 索引映射
这样即可实现跨 PHP 服务(哪怕使用不同框架)的日志格式完全统一、链路可追踪。