强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PHP 完全指南 / 第 21 章 — 日志

第 21 章 — 日志:Monolog、PSR-3 与结构化日志

21.1 PSR-3 日志接口

<?php
// PSR-3 定义了 8 个日志级别
interface LoggerInterface
{
    public function emergency(string|\Stringable $message, array $context = []): void;
    public function alert(string|\Stringable $message, array $context = []): void;
    public function critical(string|\Stringable $message, array $context = []): void;
    public function error(string|\Stringable $message, array $context = []): void;
    public function warning(string|\Stringable $message, array $context = []): void;
    public function notice(string|\Stringable $message, array $context = []): void;
    public function info(string|\Stringable $message, array $context = []): void;
    public function debug(string|\Stringable $message, array $context = []): void;
    public function log($level, string|\Stringable $message, array $context = []): void;
}
级别常量数值说明
EmergencyLogLevel::EMERGENCY600系统不可用
AlertLogLevel::ALERT550需要立即处理
CriticalLogLevel::CRITICAL500严重错误
ErrorLogLevel::ERROR400运行时错误
WarningLogLevel::WARNING300警告
NoticeLogLevel::NOTICE250重要但正常
InfoLogLevel::INFO200关键事件
DebugLogLevel::DEBUG100调试信息

21.2 Monolog

composer require monolog/monolog

21.2.1 基本用法

<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;

$logger = new Logger('app');

// 文件处理器
$logger->pushHandler(new StreamHandler(
    '/var/log/php/app.log',
    Logger::DEBUG
));

// 使用
$logger->info('User logged in', ['user_id' => 42]);
$logger->error('Payment failed', ['order_id' => 'ORD-001']);

21.2.2 多处理器

<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Handler\RotatingFileHandler;

$logger = new Logger('app');

// Debug 及以上写文件
$logger->pushHandler(new RotatingFileHandler(
    '/var/log/php/app.log',
    30,          // 保留 30 天
    Logger::DEBUG
));

// Error 及以上发送 Slack
$logger->pushHandler(new SlackWebhookHandler(
    'https://hooks.slack.com/services/xxx',
    '#alerts',
    'PHP Logger',
    true,
    null,
    Logger::ERROR
));

// Critical 及以上写紧急日志
$logger->pushHandler(new StreamHandler(
    '/var/log/php/critical.log',
    Logger::CRITICAL
));

21.2.3 常用 Handler

Handler说明
StreamHandler写入流(文件、php://output)
RotatingFileHandler按日期轮转文件
SyslogHandler写入系统日志
ErrorLogHandler写入 PHP error_log
SlackWebhookHandler发送到 Slack
RedisHandler写入 Redis
SocketHandler写入 TCP/UDP 套接字
FingersCrossedHandler条件触发(错误时才记录)
BufferHandler缓冲后批量写入
DeduplicationHandler去重

21.3 结构化日志

<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\PsrLogMessageProcessor;
use Monolog\Processor\WebProcessor;
use Monolog\Processor\MemoryUsageProcessor;

$logger = new Logger('app');

// 添加处理器链
$logger->pushProcessor(new PsrLogMessageProcessor());
$logger->pushProcessor(new WebProcessor());        // 自动添加 request 信息
$logger->pushProcessor(new MemoryUsageProcessor()); // 自动添加内存使用

// 自定义处理器
$logger->pushProcessor(function (array $record): array {
    $record['extra']['app_version'] = '1.0.0';
    $record['extra']['hostname']    = gethostname();
    $record['extra']['request_id']  = $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid();
    return $record;
});

// JSON 格式
$handler = new StreamHandler('/var/log/php/app.json');
$handler->setFormatter(new \Monolog\Formatter\JsonFormatter());
$logger->pushHandler($handler);

21.4 业务场景:请求日志中间件

<?php
declare(strict_types=1);

class RequestLoggingMiddleware
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $requestId = bin2hex(random_bytes(16));
        $startTime = microtime(true);

        $this->logger->info('Request started', [
            'request_id' => $requestId,
            'method'     => $request->getMethod(),
            'uri'        => (string) $request->getUri(),
            'ip'         => $request->getServerParams()['REMOTE_ADDR'] ?? '',
        ]);

        try {
            $response = $handler->handle($request);
        } catch (\Throwable $e) {
            $this->logger->error('Request failed', [
                'request_id' => $requestId,
                'exception'  => get_class($e),
                'message'    => $e->getMessage(),
            ]);
            throw $e;
        }

        $elapsed = round((microtime(true) - $startTime) * 1000, 2);
        $this->logger->info('Request completed', [
            'request_id' => $requestId,
            'status'     => $response->getStatusCode(),
            'elapsed_ms' => $elapsed,
        ]);

        return $response->withHeader('X-Request-ID', $requestId);
    }
}

21.5 扩展阅读


上一章第 20 章 — 测试 下一章第 22 章 — 安全