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

PHP 自定義錯誤日誌實作:統一格式、分級記錄與效能優化

     編輯:PHP 綜合知識

為什麼需要自定義錯誤日誌?

PHP 內建錯誤日誌功能雖方便,但在團隊協作、大型專案或高流量系統中,往往面臨以下問題:

  • 格式不統一,難以自動化分析、監控告警。
  • 大量 Notice/Warning 混雜,佔用磁碟空間,關鍵錯誤被淹沒。
  • 缺乏上下文資訊(請求 ID、使用者 IP、追蹤堆疊),排查困難。

自定義錯誤處理器能解決上述問題,建立結構化、可查詢、可維運的日誌體系。

啟用 PHP 內建錯誤日誌(基礎設定)

; php.ini
log_errors = On
error_log = /var/log/php/error.log
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

修改後需重啟 Web 伺服器(Apache/Nginx + PHP-FPM)。

自定義錯誤處理器完整實作

logDir = rtrim($logDir, '/');
        $this->requestId = $this->generateRequestId();
        $this->register();
    }

    private function generateRequestId(): string
    {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

    public function register(): void
    {
        set_error_handler([$this, 'handleError']);
        set_exception_handler([$this, 'handleException']);
        register_shutdown_function([$this, 'handleFatalError']);
    }

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

        $level = $this->getLevelName($errno);
        $logFile = $this->getLogFile($level);

        $record = [
            'timestamp'   => date('c'),
            'request_id'  => $this->requestId,
            'level'       => $level,
            'message'     => $errstr,
            'file'        => $errfile,
            'line'        => $errline,
            'trace'       => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5),
            'context'     => [
                'url'       => $_SERVER['REQUEST_URI'] ?? 'cli',
                'method'    => $_SERVER['REQUEST_METHOD'] ?? 'cli',
                'ip'        => $_SERVER['REMOTE_ADDR'] ?? 'cli',
                'user_agent'=> $_SERVER['HTTP_USER_AGENT'] ?? 'cli',
            ],
        ];

        $this->writeLog($logFile, $record);

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

    public function handleException(Throwable $e): void
    {
        $this->handleError(E_USER_ERROR, $e->getMessage(), $e->getFile(), $e->getLine());
    }

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

    private function getLevelName(int $errno): string
    {
        return match ($errno) {
            E_ERROR, E_USER_ERROR, E_CORE_ERROR, E_COMPILE_ERROR => 'ERROR',
            E_WARNING, E_USER_WARNING, E_CORE_WARNING, E_COMPILE_WARNING => 'WARNING',
            E_NOTICE, E_USER_NOTICE => 'NOTICE',
            E_DEPRECATED, E_USER_DEPRECATED => 'DEPRECATED',
            E_STRICT => 'STRICT',
            default => 'UNKNOWN',
        };
    }

    private function getLogFile(string $level): string
    {
        $date = date('Ymd');
        return "{$this->logDir}/php_{$level}_{$date}.log";
    }

    private function writeLog(string $file, array $record): void
    {
        // 確保目錄存在
        $dir = dirname($file);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        $json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        file_put_contents($file, $json . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}

// 初始化(建議放在 bootstrap/autoload 階段)
new StructuredErrorLogger();

關鍵設計重點解析

特性實作方式效益
分級存檔依等級產生 php_ERROR_20240115.log 等檔案快速過濾關鍵錯誤,避免單一檔案過大
JSON 格式json_encode() + FILE_APPEND | LOCK_EX支援 ELK、Loki 等日誌平台直接收集解析
請求追蹤 IDUUID v4 隨機產生,貫穿同一請求串聯分散式服務日誌,快速定位問題鏈路
上下文資訊URL、Method、IP、User-Agent、堆疊追蹤重現問題場景,減少猜測時間
致命錯誤捕獲register_shutdown_function + error_get_last()補足 set_error_handler 無法捕獲 E_ERROR 等致命錯誤

進階:手動記錄業務日誌

// 在業務程式碼中手動記錄
function logBusiness(string $message, array $context = [], string $level = 'INFO'): void
{
    $record = [
        'timestamp'  => date('c'),
        'request_id' => $GLOBALS['error_logger']->requestId ?? 'unknown',
        'level'      => strtoupper($level),
        'message'    => $message,
        'context'    => $context,
    ];
    $file = '/var/log/php/app/php_BUSINESS_' . date('Ymd') . '.log';
    file_put_contents($file, json_encode($record, JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND | LOCK_EX);
}

// 使用範例
logBusiness('訂單建立成功', ['order_id' => 12345, 'user_id' => 678], 'INFO');
logBusiness('支付閘道回呼失敗', ['gateway' => 'newebpay', 'error' => $e->getMessage()], 'WARNING');

日誌輪轉與清理(Logrotate 設定)

# /etc/logrotate.d/php-app
/var/log/php/app/php_*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 0644 www-data www-data
    sharedscripts
    postrotate
        systemctl reload php8.2-fpm > /dev/null 2>&1 || true
    endscript
}

建立完善的自定義錯誤日誌體系,是專案從「能跑」邁向「可維運」的關鍵一步。

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