PHP项目日志格式如何全服务统一规范编写

wen PHP项目 26

本文目录导读:

PHP项目日志格式如何全服务统一规范编写

  1. 统一日志格式标准(推荐 JSON 结构)
  2. PHP 侧统一实现方案
  3. 统一输出目标(日志采集与传输)
  4. 多服务统一保证的要点
  5. 禁止行为与最佳实践
  6. 完整示例:一次请求的日志输出
  7. 总结操作步骤

为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_idstack 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/..."}}

总结操作步骤

  1. 抽离日志配置类(像上面的 LogConfig),打包为公司内部 Composer 包
  2. 所有服务统一引用该包
  3. 中间件统一注入 trace_id + 环境 + 服务名
  4. 所有日志调用使用 Log::info() / Log::error() 门面
  5. 日志收集端统一采集 JSON 字段,建立 ELK/Loki 索引映射

这样即可实现跨 PHP 服务(哪怕使用不同框架)的日志格式完全统一、链路可追踪。

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