本文目录导读:

为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 服务(哪怕使用不同框架)的日志格式完全统一、链路可追踪。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播