PHP 项目日志链路ID传递

wen PHP项目 2

本文目录导读:

PHP 项目日志链路ID传递

  1. 最基础方案:单请求内(使用 $_SERVER
  2. 使用 Monolog(主要方案)
  3. 完整链路传递(跨服务 / 跨进程)
  4. 框架集成示例
  5. 跨语言/跨平台传递(标准)
  6. 最佳实践建议
  7. 完整示例(单文件简单实现)

在 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 / 其他框架

类似,在入口文件或中间件中处理。


跨语言/跨平台传递(标准)

推荐使用 OpenTelemetryZipkin 的标准:

标准 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;
}

最佳实践建议

  1. 使用 32 位十六进制(128bit)作为 Trace ID,避免碰撞
  2. 遵循 W3C 标准,便于与其他语言(Java/Go/Node.js)互通
  3. 在响应头中返回 Trace ID,方便客户端反馈问题
  4. 日志统一 JSON 格式,便于日志系统(ELK、Loki)检索
  5. 异步任务传递:在使用 Job/Queue 时,将 Trace ID 作为参数传递
  6. 不要过度设计:单一服务、小项目直接用 $_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)

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