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 等日誌平台直接收集解析 |
| 請求追蹤 ID | UUID 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');
# /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
}
建立完善的自定義錯誤日誌體系,是專案從「能跑」邁向「可維運」的關鍵一步。