首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 自訂錯誤日誌系統:分級記錄、結構化輸出、Log Rotation 與 Monolog 整合

     編輯:PHP 相關知識

PHP 內建 error_log() 功能單薄,無法滿足生產環境「分級、結構化、輪轉、多通道」需求。本文從零建構符合 PSR-3 的日誌系統,涵蓋錯誤處理器、關機函數、日誌格式化、檔案輪轉、最後整合業界標準 Monolog。

第一階段:原生實作 (零依賴)

1. 統一錯誤處理器

class Logger
{
    private string $logDir;
    private int $maxFiles;
    private array $levelMap = [
        E_ERROR             => 'emergency',
        E_WARNING           => 'warning',
        E_PARSE             => 'critical',
        E_NOTICE            => 'notice',
        E_CORE_ERROR        => 'emergency',
        E_CORE_WARNING      => 'warning',
        E_COMPILE_ERROR     => 'critical',
        E_COMPILE_WARNING   => 'warning',
        E_USER_ERROR        => 'error',
        E_USER_WARNING      => 'warning',
        E_USER_NOTICE       => 'notice',
        E_STRICT            => 'info',
        E_RECOVERABLE_ERROR => 'error',
        E_DEPRECATED        => 'debug',
        E_USER_DEPRECATED   => 'debug',
    ];

    public function __construct(string $logDir = __DIR__ . '/logs', int $maxFiles = 30)
    {
        $this->logDir = $logDir;
        $this->maxFiles = $maxFiles;
        if (!is_dir($logDir)) mkdir($logDir, 0755, true);
    }

    public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
    {
        // 忽略 error_reporting 之外的錯誤
        if (!(error_reporting() & $errno)) return false;

        $level = $this->levelMap[$errno] ?? 'error';
        $this->write($level, $errstr, [
            'file' => $errfile,
            'line' => $errline,
            'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
        ]);

        // 致命錯誤不繼續執行
        if (in_array($errno, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
            exit(1);
        }
        return true; // 不讓內建處理器處理
    }

    public function handleFatal(): void
    {
        $error = error_get_last();
        if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            $this->write('emergency', $error['message'], [
                'file' => $error['file'],
                'line' => $error['line'],
            ]);
        }
    }

    private function write(string $level, string $message, array $context = []): void
    {
        $date = new DateTimeImmutable();
        $filename = $this->logDir . '/' . $date->format('Y-m-d') . '.log';

        $record = [
            'timestamp' => $date->format('c'),
            'level' => strtoupper($level),
            'message' => $message,
            'context' => $context,
            'channel' => 'app',
        ];

        $line = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";

        // 原子寫入 (臨時檔 + rename)
        $tmp = tempnam($this->logDir, 'log_');
        file_put_contents($tmp, $line, LOCK_EX);
        rename($tmp, $filename); // 同檔案系統下原子

        // Log Rotation: 刪除超過最大檔案數的舊日誌
        $this->rotate();
    }

    private function rotate(): void
    {
        $files = glob($this->logDir . '/*.log');
        usort($files, fn($a, $b) => filemtime($a) <=> filemtime($b));
        while (count($files) > $this->maxFiles) {
            unlink(array_shift($files));
        }
    }
}

// 註冊
$logger = new Logger();
set_error_handler([$logger, 'handleError']);
register_shutdown_function([$logger, 'handleFatal']);

// 手動記錄業務日誌
$logger->write('info', 'User login', ['user_id' => 123, 'ip' => $_SERVER['REMOTE_ADDR']]);

第二階段:結構化 JSON 日誌 (ELK/EFK 友善)

上方 write() 已輸出 JSON Lines 格式,可直接被 Filebeat/Fluentd 採集送入 Elasticsearch,支援全文檢索、儀表板分析。

第三階段:整合 Monolog (生產環境標準)

composer require monolog/monolog
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
use Monolog\Processor\PsrLogMessageProcessor;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\MemoryUsageProcessor;

$log = new Logger('app');

// 主通道:每日輪轉,保留 30 天,JSON 格式
$handler = new RotatingFileHandler(__DIR__ . '/logs/app.log', 30, Logger::DEBUG);
$handler->setFormatter(new JsonFormatter());
$handler->pushProcessor(new IntrospectionProcessor(Logger::DEBUG, ['Monolog\\']));
$handler->pushProcessor(new MemoryUsageProcessor());
$log->pushHandler($handler);

// 錯誤專用通道:單獨檔案,方便告警
$errorHandler = new RotatingFileHandler(__DIR__ . '/logs/error.log', 30, Logger::ERROR);
$errorHandler->setFormatter(new JsonFormatter());
$log->pushHandler($errorHandler);

// 註冊為全域錯誤處理器
$log->pushHandler(new Monolog\Handler\ErrorHandlerHandler(Logger::ERROR));

// 使用
$log->info('Order created', ['order_id' => 123, 'amount' => 99.9]);
$log->error('Payment failed', ['order_id' => 123, 'gateway' => 'stripe', 'error' => $e->getMessage()]);

進階:多通道策略

通道級別用途保留
app (主)DEBUG+所有業務日誌30 天
errorERROR+錯誤聚焦、告警來源90 天
accessINFOHTTP 存取日誌 (NGINX 已有,應用層可補)7 天
auditNOTICE敏感操作審計 (登入、權限變更、刪除)365 天
slowWARNING慢查詢、慢 API (超過閾值)30 天

關鍵最佳實踐

  1. 永遠使用結構化 JSON:機器可解析,支援索引查詢。
  2. Context 必須包含:trace_id (分散式追蹤)、user_id、ip、request_id。
  3. 敏感資料過濾:Monolog FingersCrossedHandler 或自訂 Processor 移除密碼、Token、信用卡號。
  4. 非同步寫入:高併發下使用 BufferHandler + SwiftMailerHandler 或 Kafka/RabbitMQ 通道。
  5. 測試環境關閉檔案寫入:使用 NullHandler 或輸出到 stdout。

從原生實作理解日誌核心機制,再遷移至 Monolog 獲得生產級穩健度、擴充性與生態整合,是 PHP 專案日誌演進的標準路徑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved