本文目录导读:

在 PHP 项目中实现日志链路 ID(Trace ID / Request ID)传递,主要目的是追踪一次完整的请求链路,尤其是在微服务架构或异步任务中,以下是几种从简单到复杂的实现方案:
最基础方案:单请求内(使用 $_SERVER)
适用于单体应用,只需在单个 HTTP 请求内保持同一个 ID。
生成/获取 Trace ID
<?php
class TraceContext
{
private static string $traceId = '';
public static function getTraceId(): string
{
if (self::$traceId === '') {
self::$traceId = self::generateTraceId();
}
return self::$traceId;
}
public static function setTraceId(string $traceId): void
{
self::$traceId = $traceId;
}
private static function generateTraceId(): string
{
// 优先从请求头获取(方便链路追踪)
$headerTraceId = $_SERVER['HTTP_X_TRACE_ID'] ?? '';
if ($headerTraceId && preg_match('/^[a-f0-9]{32}$/', $headerTraceId)) {
return $headerTraceId;
}
// 生成唯一 ID(使用 openssl 更安全)
return bin2hex(random_bytes(16)); // 32位十六进制
// 或者:return uniqid('', true); // 但不太可靠
// 或者:return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', ...); // UUID
}
}
在入口文件中初始化(如 index.php)
<?php // index.php require_once 'TraceContext.php'; // 在业务逻辑开始前设置 TraceContext::setTraceId(TraceContext::getTraceId()); // 后续所有日志都带上 trace_id
自定义日志函数
<?php
function log_info(string $message, array $context = []): void
{
$traceId = TraceContext::getTraceId();
$logData = [
'time' => date('Y-m-d H:i:s'),
'trace_id' => $traceId,
'level' => 'INFO',
'message' => $message,
'context' => $context,
];
// 输出 JSON 日志,方便日志系统解析
echo json_encode($logData, JSON_UNESCAPED_UNICODE) . PHP_EOL;
// 或者写入日志文件
// file_put_contents('/var/log/app.log', json_encode($logData) . PHP_EOL, FILE_APPEND);
}
使用 Monolog(主要方案)
大多数 PHP 框架(Laravel、Symfony)使用 Monolog,可以通过 Processor 添加 Trace ID。
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class TraceIdProcessor
{
public function __invoke(array $record): array
{
$record['extra']['trace_id'] = TraceContext::getTraceId();
$record['extra']['span_id'] = uniqid('span_', true); // 可选的 Span ID
return $record;
}
}
// 初始化 Logger
$logger = new Logger('app');
$logger->pushProcessor(new TraceIdProcessor());
$logger->pushHandler(new StreamHandler(__DIR__ . '/app.log', Logger::DEBUG));
// 业务中使用
$logger->info('用户下单', ['order_id' => 12345]);
// 输出: {"message":"用户下单","context":{"order_id":12345},"extra":{"trace_id":"abc123..."}}
完整链路传递(跨服务 / 跨进程)
适用于微服务架构,需要将 Trace ID 通过 HTTP 头、消息队列、RPC 等传递。
客户端传递(调用其他服务时)
<?php
class HttpClient
{
public static function get(string $url, array $headers = []): string
{
$traceId = TraceContext::getTraceId();
$headers['X-Trace-Id'] = $traceId;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, self::buildHeaders($headers));
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
服务端接收(从请求头提取)
<?php
// framework/entry.php 或 middleware
$traceId = $_SERVER['HTTP_X_TRACE_ID'] ?? '';
if (preg_match('/^[a-f0-9]{32}$/', $traceId)) {
TraceContext::setTraceId($traceId);
} else {
TraceContext::setTraceId(TraceContext::generateTraceId());
}
MQ 消息传递(如 RabbitMQ、Kafka)
<?php
// 发送消息时
$message = new AMQPMessage($body, [
'headers' => ['X-Trace-Id' => TraceContext::getTraceId()]
]);
// 消费消息时
$traceId = $message->get('application_headers')->getNativeData()['X-Trace-Id'] ?? '';
TraceContext::setTraceId($traceId);
框架集成示例
Laravel(中间件方式)
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class TraceIdMiddleware
{
public function handle(Request $request, Closure $next)
{
$traceId = $request->header('X-Trace-Id', '');
if (!preg_match('/^[a-f0-9]{32}$/', $traceId)) {
$traceId = bin2hex(random_bytes(16));
}
// 存入容器,全局可用
app()->instance('trace_id', $traceId);
$response = $next($request);
// 响应头也带上,方便前端排查
$response->headers->set('X-Trace-Id', $traceId);
return $response;
}
}
ThinkPHP / 其他框架
类似,在入口文件或中间件中处理。
跨语言/跨平台传递(标准)
推荐使用 OpenTelemetry 或 Zipkin 的标准:
| 标准 | Header 名称 | 示例值 |
|---|---|---|
| W3C Trace-Context | traceparent |
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 |
| OpenTelemetry | tracestate |
vendor1=value1,vendor2=value2 |
| 自定义 | X-Trace-Id |
abc123... |
| 阿里云 | eagleeye-traceid |
1a2b3c... |
W3C 标准示例
<?php
// 生成 W3C traceparent
function generateTraceParent(): string
{
$traceId = bin2hex(random_bytes(16)); // 32位
$spanId = bin2hex(random_bytes(8)); // 16位
return "00-{$traceId}-{$spanId}-01";
}
// 解析
function parseTraceParent(string $header): ?array
{
if (preg_match('/^00-([a-f0-9]{32})-([a-f0-9]{16})-[01]$/', $header, $matches)) {
return [
'trace_id' => $matches[1],
'span_id' => $matches[2],
];
}
return null;
}
最佳实践建议
- 使用 32 位十六进制(128bit)作为 Trace ID,避免碰撞
- 遵循 W3C 标准,便于与其他语言(Java/Go/Node.js)互通
- 在响应头中返回 Trace ID,方便客户端反馈问题
- 日志统一 JSON 格式,便于日志系统(ELK、Loki)检索
- 异步任务传递:在使用 Job/Queue 时,将 Trace ID 作为参数传递
- 不要过度设计:单一服务、小项目直接用
$_SERVER['REQUEST_TIME_FLOAT']或uniqid()即可
完整示例(单文件简单实现)
<?php
class Trace
{
private static string $id = '';
public static function id(): string
{
if (self::$id === '') {
self::$id = $_SERVER['HTTP_X_TRACE_ID'] ?? '';
if (!preg_match('/^[a-f0-9]{32}$/', self::$id)) {
self::$id = bin2hex(random_bytes(16));
}
}
return self::$id;
}
public static function header(): string
{
return "X-Trace-Id: " . self::id();
}
}
// 日志函数
function log_error(string $msg, array $ctx = []): void
{
$log = [
'timestamp' => date(DATE_ATOM),
'trace_id' => Trace::id(),
'level' => 'ERROR',
'message' => $msg,
'context' => $ctx,
];
error_log(json_encode($log, JSON_UNESCAPED_UNICODE));
}
// HTTP 调用
function http_get(string $url): string
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [Trace::header()]);
$res = curl_exec($ch);
curl_close($ch);
return $res;
}
// 使用
Trace::id();
log_error('订单创建失败', ['order' => 10086]);
- 单体应用:简单生成一个 ID 存入全局即可
- 多服务应用:必须通过 HTTP 头/MQ 传递,保证链路一致
- 标准选择:优先 W3C Trace-Context,或各云厂商标准
- 落地关键:日志系统支持按 trace_id 聚合查询(如 ELK、Loki)