set_exception_logger() 是一个自定义的PHP函数概念,用于设置专门记录异常到日志的自定义处理器。虽然PHP没有内置这个函数,但我们可以创建自己的实现,用于统一处理异常的日志记录。
set_exception_handler(),我们可以基于它创建专门的异常日志记录器。
确保所有异常都按照统一的格式记录到日志,便于后续分析和处理。
支持将异常记录到文件、数据库、Elasticsearch等多种存储介质。
根据异常级别自动发送邮件、短信、Slack等通知。
便于收集异常数据,进行统计分析和性能监控。
我们可以设计一个set_exception_logger()函数,它接受一个可调用对象(回调函数),当异常发生时,这个回调函数会被调用来记录异常日志。
bool set_exception_logger(callable $logger, int $log_level = E_ALL)
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| $logger | callable | 必填 | 可调用对象,用于记录异常日志。这个函数应该接受一个异常对象作为参数。 |
| $log_level | int | E_ALL | 指定要记录哪些级别的异常。可以使用位掩码组合。 |
bool - 如果成功设置返回true,失败返回false。
实现一个简单的set_exception_logger()函数:
<?php
/**
* 设置异常日志记录器
*
* @param callable $logger 异常日志记录回调函数
* @param int $log_level 日志级别
* @return bool
*/
function set_exception_logger(callable $logger, int $log_level = E_ALL) {
// 保存之前的异常处理器
$previous_handler = set_exception_handler(
function($exception) use ($logger, $log_level) {
// 检查异常级别
if ($log_level & E_ALL) {
// 调用日志记录器
$logger($exception);
}
// 调用之前的异常处理器(如果有)
if ($previous_handler !== null) {
$previous_handler($exception);
} else {
// 如果没有之前的处理器,重新抛出异常
throw $exception;
}
}
);
return $previous_handler !== null;
}
// 使用示例
echo "<h4>测试set_exception_logger()基本实现:</h4>";
// 创建一个简单的日志记录器
$simpleLogger = function($exception) {
$log = sprintf(
"[%s] 异常: %s in %s on line %d\n",
date('Y-m-d H:i:s'),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
);
// 写入文件
file_put_contents('exception.log', $log, FILE_APPEND);
echo "<div style='background:#f8f9fa; padding:10px; margin:5px; border:1px solid #ddd;'>";
echo "异常已记录到日志: " . htmlspecialchars($exception->getMessage());
echo "</div>";
};
// 设置异常日志记录器
set_exception_logger($simpleLogger);
// 抛出一个异常来测试
throw new Exception("这是一个测试异常");
// 注意:这行代码不会执行,因为异常会导致脚本终止
echo "这行代码不会执行";
创建一个完整的异常日志记录器类:
<?php
/**
* 异常日志记录器类
*/
class ExceptionLogger {
private $logFile = 'logs/exceptions.log';
private $logLevel = E_ALL;
private $handlers = [];
private static $instance = null;
private function __construct() {
// 私有构造函数,单例模式
}
/**
* 获取单例实例
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 设置异常日志记录器
*/
public function setLogger(callable $logger, int $log_level = E_ALL) {
$this->logLevel = $log_level;
// 保存之前的异常处理器
$previous_handler = set_exception_handler(
function($exception) use ($logger) {
$this->handleException($exception, $logger);
}
);
// 如果有之前的处理器,保存它
if ($previous_handler !== null) {
$this->handlers[] = $previous_handler;
}
return $this;
}
/**
* 处理异常
*/
private function handleException($exception, $logger) {
// 记录异常
$logger($exception);
// 调用之前保存的处理器
foreach ($this->handlers as $handler) {
if (is_callable($handler)) {
$handler($exception);
}
}
// 如果没有其他处理器,重新抛出异常
if (empty($this->handlers)) {
throw $exception;
}
}
/**
* 设置日志文件
*/
public function setLogFile($file) {
$this->logFile = $file;
// 确保目录存在
$dir = dirname($file);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
return $this;
}
/**
* 获取默认的日志记录器
*/
public function getDefaultLogger() {
return function($exception) {
$log = $this->formatExceptionLog($exception);
file_put_contents($this->logFile, $log, FILE_APPEND);
};
}
/**
* 格式化异常日志
*/
private function formatExceptionLog($exception) {
$timestamp = date('Y-m-d H:i:s');
$exceptionClass = get_class($exception);
$message = $exception->getMessage();
$file = $exception->getFile();
$line = $exception->getLine();
$code = $exception->getCode();
$trace = $exception->getTraceAsString();
$log = "========================================\n";
$log .= "时间: $timestamp\n";
$log .= "异常类型: $exceptionClass\n";
$log .= "代码: $code\n";
$log .= "消息: $message\n";
$log .= "位置: $file:$line\n";
$log .= "请求URI: " . ($_SERVER['REQUEST_URI'] ?? 'CLI') . "\n";
$log .= "用户代理: " . ($_SERVER['HTTP_USER_AGENT'] ?? '未知') . "\n";
$log .= "IP地址: " . ($_SERVER['REMOTE_ADDR'] ?? '未知') . "\n";
$log .= "堆栈跟踪:\n$trace\n";
$log .= "========================================\n\n";
return $log;
}
/**
* 创建JSON格式的日志记录器
*/
public function getJsonLogger() {
return function($exception) {
$log = [
'timestamp' => date('c'),
'type' => 'exception',
'exception_class' => get_class($exception),
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTrace(),
'request' => [
'uri' => $_SERVER['REQUEST_URI'] ?? null,
'method' => $_SERVER['REQUEST_METHOD'] ?? null,
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
],
'environment' => getenv('APP_ENV') ?? 'production',
];
$json = json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
file_put_contents($this->logFile, $json, FILE_APPEND);
};
}
/**
* 创建发送邮件的日志记录器
*/
public function getEmailLogger($recipients, $subject = '系统异常报告') {
return function($exception) use ($recipients, $subject) {
// 首先记录到文件
$fileLogger = $this->getDefaultLogger();
$fileLogger($exception);
// 发送邮件通知
$body = "系统发生异常:\n\n";
$body .= "时间: " . date('Y-m-d H:i:s') . "\n";
$body .= "异常类型: " . get_class($exception) . "\n";
$body .= "消息: " . $exception->getMessage() . "\n";
$body .= "位置: " . $exception->getFile() . ":" . $exception->getLine() . "\n";
$body .= "请求URI: " . ($_SERVER['REQUEST_URI'] ?? 'CLI') . "\n";
// 在实际应用中,这里会调用邮件发送函数
// mail(implode(',', $recipients), $subject, $body);
error_log("邮件通知已发送: " . $body);
};
}
}
// 使用示例
echo "<h4>使用ExceptionLogger类:</h4>";
// 获取实例
$logger = ExceptionLogger::getInstance();
// 设置日志文件
$logger->setLogFile('logs/app_exceptions.log');
// 设置默认日志记录器
$logger->setLogger($logger->getDefaultLogger());
echo "<div class='alert alert-success'>";
echo "异常日志记录器已设置,日志将保存到 logs/app_exceptions.log";
echo "</div>";
// 测试异常
try {
throw new RuntimeException("测试运行时异常");
} catch (Exception $e) {
// 这个异常会被catch捕获,不会触发异常处理器
echo "<div class='alert alert-warning'>";
echo "异常被catch捕获: " . $e->getMessage();
echo "</div>";
}
// 这个异常不会被捕获,会触发异常处理器
throw new Exception("未捕获的测试异常");
创建一个支持文件、数据库、Elasticsearch等多种存储后端的日志记录器:
<?php
/**
* 异常日志管理器
*/
class ExceptionLogManager {
private $backends = [];
private $logLevel = E_ALL;
/**
* 添加日志后端
*/
public function addBackend($backend, $level = E_ALL) {
$this->backends[] = [
'backend' => $backend,
'level' => $level
];
return $this;
}
/**
* 设置异常日志记录器
*/
public function setLogger() {
$previous_handler = set_exception_handler(
function($exception) {
$this->logException($exception);
}
);
return $previous_handler !== null;
}
/**
* 记录异常
*/
private function logException($exception) {
foreach ($this->backends as $backendInfo) {
$backend = $backendInfo['backend'];
$level = $backendInfo['level'];
// 检查是否应该记录这个级别的异常
if ($this->shouldLog($exception, $level)) {
$backend->log($exception);
}
}
}
/**
* 检查是否应该记录异常
*/
private function shouldLog($exception, $level) {
// 这里可以根据异常类型、消息等决定是否记录
return true;
}
}
/**
* 日志后端接口
*/
interface LogBackend {
public function log($exception);
}
/**
* 文件日志后端
*/
class FileLogBackend implements LogBackend {
private $logFile;
public function __construct($logFile) {
$this->logFile = $logFile;
// 确保目录存在
$dir = dirname($logFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
public function log($exception) {
$log = sprintf(
"[%s] [FILE] %s: %s in %s:%d\n",
date('Y-m-d H:i:s'),
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
);
file_put_contents($this->logFile, $log, FILE_APPEND);
}
}
/**
* 数据库日志后端
*/
class DatabaseLogBackend implements LogBackend {
private $pdo;
private $table = 'exception_logs';
public function __construct(PDO $pdo, $table = null) {
$this->pdo = $pdo;
if ($table !== null) {
$this->table = $table;
}
$this->createTableIfNotExists();
}
private function createTableIfNotExists() {
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
id INT AUTO_INCREMENT PRIMARY KEY,
exception_class VARCHAR(255),
message TEXT,
code INT,
file VARCHAR(500),
line INT,
trace TEXT,
created_at DATETIME,
request_uri VARCHAR(500),
ip_address VARCHAR(45),
user_agent TEXT
)";
$this->pdo->exec($sql);
}
public function log($exception) {
$sql = "INSERT INTO {$this->table}
(exception_class, message, code, file, line, trace, created_at, request_uri, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
get_class($exception),
$exception->getMessage(),
$exception->getCode(),
$exception->getFile(),
$exception->getLine(),
$exception->getTraceAsString(),
date('Y-m-d H:i:s'),
$_SERVER['REQUEST_URI'] ?? null,
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null
]);
}
}
/**
* Elasticsearch日志后端
*/
class ElasticsearchLogBackend implements LogBackend {
private $client;
private $index;
public function __construct($client, $index = 'exceptions') {
$this->client = $client;
$this->index = $index;
}
public function log($exception) {
$document = [
'timestamp' => date('c'),
'exception_class' => get_class($exception),
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTrace(),
'request' => [
'uri' => $_SERVER['REQUEST_URI'] ?? null,
'method' => $_SERVER['REQUEST_METHOD'] ?? null,
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
],
'environment' => getenv('APP_ENV') ?? 'production',
];
// 在实际应用中,这里会调用Elasticsearch客户端
// $this->client->index([
// 'index' => $this->index,
// 'body' => $document
// ]);
error_log("Elasticsearch日志: " . json_encode($document));
}
}
// 使用示例
echo "<h4>使用多种存储后端的日志记录器:</h4>";
// 创建日志管理器
$logManager = new ExceptionLogManager();
// 添加文件后端
$fileBackend = new FileLogBackend('logs/exceptions.log');
$logManager->addBackend($fileBackend);
// 模拟数据库连接(实际使用时需要真实的PDO连接)
try {
$pdo = new PDO('sqlite::memory:');
$dbBackend = new DatabaseLogBackend($pdo);
$logManager->addBackend($dbBackend);
} catch (Exception $e) {
echo "<div class='alert alert-warning'>";
echo "数据库连接失败: " . $e->getMessage();
echo "</div>";
}
// 添加Elasticsearch后端(模拟)
$esBackend = new ElasticsearchLogBackend(null);
$logManager->addBackend($esBackend);
// 设置日志记录器
$logManager->setLogger();
echo "<div class='alert alert-success'>";
echo "多后端异常日志记录器已设置,异常将同时记录到文件、数据库和Elasticsearch";
echo "</div>";
// 触发异常测试
trigger_error("这是一个测试错误", E_USER_WARNING);
// 注意:这个异常会被所有后端记录
throw new RuntimeException("测试多后端日志记录");
演示在实际项目中如何使用异常日志记录器:
<?php
/**
* 应用程序异常日志系统
*/
class AppExceptionLogger {
private static $instance = null;
private $config = [
'enabled' => true,
'log_file' => 'logs/app_exceptions.log',
'log_level' => E_ALL,
'notify_email' => null,
'notify_level' => E_USER_ERROR,
'slack_webhook' => null,
'slack_channel' => '#exceptions',
];
private function __construct() {
// 从配置文件或环境变量加载配置
$this->loadConfig();
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 加载配置
*/
private function loadConfig() {
// 可以从环境变量加载
if (getenv('EXCEPTION_LOG_FILE')) {
$this->config['log_file'] = getenv('EXCEPTION_LOG_FILE');
}
if (getenv('EXCEPTION_NOTIFY_EMAIL')) {
$this->config['notify_email'] = getenv('EXCEPTION_NOTIFY_EMAIL');
}
if (getenv('EXCEPTION_NOTIFY_LEVEL')) {
$this->config['notify_level'] = (int)getenv('EXCEPTION_NOTIFY_LEVEL');
}
if (getenv('SLACK_WEBHOOK')) {
$this->config['slack_webhook'] = getenv('SLACK_WEBHOOK');
}
}
/**
* 初始化异常日志系统
*/
public function init() {
if (!$this->config['enabled']) {
return;
}
// 设置异常处理器
set_exception_handler([$this, 'handleException']);
// 设置错误处理器,将错误转换为异常
set_error_handler([$this, 'handleError']);
// 注册关闭函数,处理致命错误
register_shutdown_function([$this, 'handleShutdown']);
return $this;
}
/**
* 处理异常
*/
public function handleException($exception) {
$this->logException($exception);
$this->notifyException($exception);
// 显示错误页面(生产环境)
if (php_sapi_name() !== 'cli') {
$this->displayErrorPage($exception);
}
exit(1);
}
/**
* 处理错误
*/
public function handleError($errno, $errstr, $errfile, $errline) {
// 将错误转换为异常
if (error_reporting() & $errno) {
$exception = new ErrorException($errstr, 0, $errno, $errfile, $errline);
throw $exception;
}
return false;
}
/**
* 处理关闭时的致命错误
*/
public function handleShutdown() {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$exception = new ErrorException(
$error['message'],
0,
$error['type'],
$error['file'],
$error['line']
);
$this->handleException($exception);
}
}
/**
* 记录异常
*/
private function logException($exception) {
$log = $this->formatLog($exception);
// 确保日志目录存在
$logDir = dirname($this->config['log_file']);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
// 写入日志文件
file_put_contents($this->config['log_file'], $log, FILE_APPEND);
// 同时记录到系统日志
error_log("应用程序异常: " . $exception->getMessage());
}
/**
* 格式化日志
*/
private function formatLog($exception) {
$timestamp = date('Y-m-d H:i:s');
$exceptionClass = get_class($exception);
$message = $exception->getMessage();
$file = $exception->getFile();
$line = $exception->getLine();
$code = $exception->getCode();
$trace = $exception->getTraceAsString();
$log = "[{$timestamp}] [{$exceptionClass}] [{$code}]\n";
$log .= "消息: {$message}\n";
$log .= "位置: {$file}:{$line}\n";
if (php_sapi_name() !== 'cli') {
$log .= "请求: " . ($_SERVER['REQUEST_URI'] ?? '未知') . "\n";
$log .= "方法: " . ($_SERVER['REQUEST_METHOD'] ?? '未知') . "\n";
$log .= "IP: " . ($_SERVER['REMOTE_ADDR'] ?? '未知') . "\n";
$log .= "用户代理: " . ($_SERVER['HTTP_USER_AGENT'] ?? '未知') . "\n";
}
$log .= "堆栈跟踪:\n{$trace}\n";
$log .= str_repeat("-", 80) . "\n";
return $log;
}
/**
* 通知异常
*/
private function notifyException($exception) {
$exceptionLevel = $this->getExceptionLevel($exception);
// 检查是否需要通知
if ($exceptionLevel & $this->config['notify_level']) {
// 发送邮件通知
if ($this->config['notify_email']) {
$this->sendEmailNotification($exception);
}
// 发送Slack通知
if ($this->config['slack_webhook']) {
$this->sendSlackNotification($exception);
}
}
}
/**
* 获取异常级别
*/
private function getExceptionLevel($exception) {
$exceptionClass = get_class($exception);
// 根据异常类型确定级别
if ($exceptionClass === 'Error' ||
$exceptionClass === 'ParseError' ||
$exceptionClass === 'TypeError') {
return E_ERROR;
}
if ($exceptionClass === 'PDOException') {
return E_USER_ERROR;
}
return E_USER_WARNING;
}
/**
* 发送邮件通知
*/
private function sendEmailNotification($exception) {
$subject = "[" . getenv('APP_ENV') . "] 系统异常: " . $exception->getMessage();
$body = "异常详情:\n\n";
$body .= "时间: " . date('Y-m-d H:i:s') . "\n";
$body .= "环境: " . (getenv('APP_ENV') ?: 'production') . "\n";
$body .= "异常类型: " . get_class($exception) . "\n";
$body .= "消息: " . $exception->getMessage() . "\n";
$body .= "位置: " . $exception->getFile() . ":" . $exception->getLine() . "\n";
if (php_sapi_name() !== 'cli') {
$body .= "请求URI: " . ($_SERVER['REQUEST_URI'] ?? '未知') . "\n";
$body .= "请求方法: " . ($_SERVER['REQUEST_METHOD'] ?? '未知') . "\n";
$body .= "用户IP: " . ($_SERVER['REMOTE_ADDR'] ?? '未知') . "\n";
}
$body .= "\n堆栈跟踪:\n" . $exception->getTraceAsString();
// 在实际应用中发送邮件
// mail($this->config['notify_email'], $subject, $body);
error_log("邮件通知内容: " . $body);
}
/**
* 发送Slack通知
*/
private function sendSlackNotification($exception) {
$payload = [
'channel' => $this->config['slack_channel'],
'username' => '异常监控机器人',
'text' => "🚨 *系统异常报告*",
'attachments' => [[
'color' => 'danger',
'fields' => [
[
'title' => '环境',
'value' => getenv('APP_ENV') ?: 'production',
'short' => true
],
[
'title' => '异常类型',
'value' => get_class($exception),
'short' => true
],
[
'title' => '异常消息',
'value' => $exception->getMessage(),
'short' => false
],
[
'title' => '位置',
'value' => $exception->getFile() . ":" . $exception->getLine(),
'short' => false
]
],
'ts' => time()
]]
];
// 在实际应用中发送Slack通知
// $ch = curl_init($this->config['slack_webhook']);
// curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
// curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
// curl_exec($ch);
// curl_close($ch);
error_log("Slack通知: " . json_encode($payload));
}
/**
* 显示错误页面
*/
private function displayErrorPage($exception) {
$env = getenv('APP_ENV') ?: 'production';
if ($env === 'development') {
// 开发环境显示详细错误
echo "<!DOCTYPE html>";
echo "<html><head><title>系统异常 - 开发模式</title><style>";
echo "body { font-family: monospace; margin: 20px; background: #f5f5f5; }";
echo ".exception { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }";
echo ".exception-header { background: #dc3545; color: white; padding: 15px; margin: -20px -20px 20px -20px; border-radius: 5px 5px 0 0; }";
echo "pre { background: #f8f9fa; padding: 15px; border: 1px solid #dee2e6; border-radius: 3px; overflow: auto; }";
echo "</style></head><body>";
echo "<div class='exception'>";
echo "<div class='exception-header'>";
echo "<h2 style='margin:0;'>" . get_class($exception) . "</h2>";
echo "</div>";
echo "<p><strong>消息:</strong> " . htmlspecialchars($exception->getMessage()) . "</p>";
echo "<p><strong>位置:</strong> " . $exception->getFile() . " 第 " . $exception->getLine() . " 行</p>";
echo "<h3>堆栈跟踪:</h3>";
echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
echo "</div>";
echo "</body></html>";
} else {
// 生产环境显示用户友好页面
header('HTTP/1.1 500 Internal Server Error');
echo "<!DOCTYPE html>";
echo "<html><head><title>系统错误</title><style>";
echo "body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; }";
echo ".error-container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); max-width: 500px; }";
echo "h1 { color: #dc3545; margin-bottom: 20px; }";
echo ".btn { display: inline-block; background: #667eea; color: white; padding: 10px 20px; border-radius: 5px; text-decoration: none; transition: background 0.3s; }";
echo ".btn:hover { background: #764ba2; }";
echo "</style></head><body>";
echo "<div class='error-container'>";
echo "<h1>系统错误</h1>";
echo "<p>抱歉,系统发生了内部错误。</p>";
echo "<p>我们的技术团队已收到通知,正在处理此问题。</p>";
echo "<p><a href='/' class='btn'>返回首页</a></p>";
echo "</div>";
echo "</body></html>";
}
}
}
// 使用示例
echo "<h4>使用AppExceptionLogger:</h4>";
// 设置环境变量
putenv('APP_ENV=development');
putenv('EXCEPTION_LOG_FILE=logs/myapp_exceptions.log');
putenv('EXCEPTION_NOTIFY_EMAIL=admin@example.com');
// 获取日志记录器实例
$logger = AppExceptionLogger::getInstance();
// 初始化异常日志系统
$logger->init();
echo "<div class='alert alert-success'>";
echo "应用程序异常日志系统已初始化";
echo "</div>";
// 定义一些自定义异常
class DatabaseException extends Exception {
public function __construct($message, $query = null, $code = 0, Exception $previous = null) {
parent::__construct($message, $code, $previous);
$this->query = $query;
}
}
class ValidationException extends Exception {
protected $errors = [];
public function __construct($message, $errors = [], $code = 0, Exception $previous = null) {
$this->errors = $errors;
parent::__construct($message, $code, $previous);
}
public function getErrors() {
return $this->errors;
}
}
// 模拟业务逻辑
function processOrder($orderData) {
if (empty($orderData['items'])) {
throw new ValidationException("订单项不能为空", ['items' => 'required']);
}
if (!isset($orderData['customer_id'])) {
throw new ValidationException("客户ID不能为空", ['customer_id' => 'required']);
}
// 模拟数据库操作
if (rand(0, 1)) {
throw new DatabaseException("数据库插入失败", "INSERT INTO orders (...) VALUES (...)");
}
return ['order_id' => rand(1000, 9999), 'status' => 'created'];
}
// 测试异常处理
echo "<h5>测试不同类型的异常:</h5>";
try {
$result = processOrder(['items' => []]);
echo "订单创建成功: " . print_r($result, true);
} catch (ValidationException $e) {
echo "<div class='alert alert-warning'>";
echo "验证异常: " . $e->getMessage();
echo "</div>";
} catch (DatabaseException $e) {
echo "<div class='alert alert-danger'>";
echo "数据库异常: " . $e->getMessage();
echo "</div>";
}
// 这个异常会被异常处理器捕获并记录
processOrder([]);
echo "<div class='alert alert-info'>";
echo "查看 logs/myapp_exceptions.log 文件获取异常日志";
echo "</div>";
演示如何使用流行的Monolog库实现异常日志记录:
<?php
// 注意:需要先安装Monolog库
// composer require monolog/monolog
// 在实际项目中,我们通常使用Monolog这样的成熟日志库
// 这里演示如何与Monolog集成
// 模拟Monolog的使用
class MockMonolog {
public static function getLogger($name = 'app') {
return new self();
}
public function error($message, array $context = []) {
error_log("[Monolog ERROR] " . $message . " " . json_encode($context));
}
public function critical($message, array $context = []) {
error_log("[Monolog CRITICAL] " . $message . " " . json_encode($context));
}
public function emergency($message, array $context = []) {
error_log("[Monolog EMERGENCY] " . $message . " " . json_encode($context));
}
}
/**
* Monolog异常日志记录器
*/
class MonologExceptionLogger {
private $logger;
public function __construct($logger = null) {
$this->logger = $logger ?: MockMonolog::getLogger('exceptions');
}
/**
* 设置异常日志记录器
*/
public function setLogger() {
$previous_handler = set_exception_handler(
function($exception) {
$this->handleException($exception);
}
);
return $this;
}
/**
* 处理异常
*/
private function handleException($exception) {
$this->logException($exception);
// 重新抛出异常,让其他处理器处理
throw $exception;
}
/**
* 记录异常
*/
private function logException($exception) {
$context = [
'exception' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'code' => $exception->getCode(),
'trace' => $exception->getTrace(),
];
// 根据异常类型选择日志级别
$level = $this->getExceptionLevel($exception);
switch ($level) {
case 'emergency':
$this->logger->emergency($exception->getMessage(), $context);
break;
case 'critical':
$this->logger->critical($exception->getMessage(), $context);
break;
case 'error':
default:
$this->logger->error($exception->getMessage(), $context);
break;
}
}
/**
* 获取异常级别
*/
private function getExceptionLevel($exception) {
$exceptionClass = get_class($exception);
// 根据异常类型确定级别
if ($exceptionClass === 'Error' ||
$exceptionClass === 'ParseError' ||
$exceptionClass === 'TypeError') {
return 'emergency';
}
if ($exceptionClass === 'PDOException' ||
$exceptionClass === 'DatabaseException') {
return 'critical';
}
return 'error';
}
}
// 使用示例
echo "<h4>使用MonologExceptionLogger:</h4>";
// 创建Monolog异常日志记录器
$monologLogger = new MonologExceptionLogger();
// 设置异常日志记录器
$monologLogger->setLogger();
echo "<div class='alert alert-success'>";
echo "Monolog异常日志记录器已设置";
echo "</div>";
// 定义不同级别的异常
class EmergencyException extends Exception {}
class CriticalException extends Exception {}
class WarningException extends Exception {}
// 测试不同级别的异常
try {
throw new EmergencyException("紧急异常测试");
} catch (Exception $e) {
echo "<div class='alert alert-danger'>";
echo "捕获异常: " . $e->getMessage();
echo "</div>";
}
try {
throw new CriticalException("严重异常测试");
} catch (Exception $e) {
echo "<div class='alert alert-warning'>";
echo "捕获异常: " . $e->getMessage();
echo "</div>";
}
// 这个异常会被异常处理器记录
throw new Exception("普通异常测试");
总结异常日志记录的最佳实践和模式:
<?php
/**
* 异常日志记录的最佳实践总结
*/
class ExceptionLoggingBestPractices {
public static function getPractices() {
return [
[
'practice' => '结构化日志',
'description' => '使用JSON或其他结构化格式记录日志,便于解析和分析',
'example' => '{
"timestamp": "2023-10-15T14:30:25+08:00",
"level": "ERROR",
"exception": "RuntimeException",
"message": "数据库连接失败",
"context": {
"host": "localhost",
"port": 3306,
"database": "myapp"
}
}',
],
[
'practice' => '上下文信息',
'description' => '记录异常发生时的上下文信息,如用户ID、请求参数等',
'example' => '// 记录上下文信息
$context = [
"user_id" => $userId,
"request_id" => $requestId,
"uri" => $_SERVER["REQUEST_URI"],
"ip" => $_SERVER["REMOTE_ADDR"]
];
$logger->error($exception->getMessage(), $context);',
],
[
'practice' => '异常链',
'description' => '记录完整的异常链,包括根本原因',
'example' => 'function logExceptionChain($exception) {
while ($exception !== null) {
$logger->error($exception->getMessage(), [
"exception" => get_class($exception),
"file" => $exception->getFile(),
"line" => $exception->getLine(),
]);
$exception = $exception->getPrevious();
}
}',
],
[
'practice' => '敏感信息过滤',
'description' => '过滤日志中的敏感信息,如密码、令牌等',
'example' => 'function filterSensitiveData($data) {
$sensitiveFields = ["password", "token", "api_key", "secret"];
foreach ($sensitiveFields as $field) {
if (isset($data[$field])) {
$data[$field] = "[FILTERED]";
}
}
return $data;
}',
],
[
'practice' => '日志轮转',
'description' => '实现日志轮转,避免日志文件过大',
'example' => '// 使用Monolog的RotatingFileHandler
$logger = new Logger("app");
$handler = new RotatingFileHandler(
"logs/app.log",
30, // 保留30天
Logger::DEBUG
);
$logger->pushHandler($handler);',
],
[
'practice' => '性能考虑',
'description' => '异步记录日志,避免阻塞主业务流程',
'example' => '// 使用队列异步记录日志
function logAsync($level, $message, $context) {
$logJob = new LogJob($level, $message, $context);
Queue::push($logJob); // 推送到消息队列
}',
],
];
}
}
// 使用示例
echo "<h4>异常日志记录的最佳实践:</h4>";
$practices = ExceptionLoggingBestPractices::getPractices();
foreach ($practices as $index => $practice) {
echo "<div style='border:1px solid #dee2e6; border-radius:5px; padding:15px; margin-bottom:15px;'>";
echo "<h5>" . ($index + 1) . ". " . $practice['practice'] . "</h5>";
echo "<p>" . $practice['description'] . "</p>";
echo "<pre style='background:#f8f9fa; padding:10px; border-radius:3px; font-size:0.9em;'>" .
htmlspecialchars($practice['example']) . "</pre>";
echo "</div>";
}
// 完整的异常日志记录器示例
echo "<h4>完整的异常日志记录器实现:</h4>";
$completeExample = '<?php
/**
* 完整的异常日志记录器
*/
class CompleteExceptionLogger {
private $logger;
private $config = [];
public function __construct(array $config = []) {
$this->config = array_merge([
"enabled" => true,
"log_level" => E_ALL,
"handlers" => [],
"processors" => [],
"filters" => [],
], $config);
}
/**
* 初始化异常日志记录器
*/
public function init() {
if (!$this->config["enabled"]) {
return;
}
// 设置异常处理器
set_exception_handler([$this, "handleException"]);
// 设置错误处理器
set_error_handler([$this, "handleError"]);
return $this;
}
/**
* 处理异常
*/
public function handleException($exception) {
$this->log($exception);
$this->notify($exception);
// 重新抛出异常
throw $exception;
}
/**
* 处理错误
*/
public function handleError($errno, $errstr, $errfile, $errline) {
if (error_reporting() & $errno) {
$exception = new ErrorException($errstr, 0, $errno, $errfile, $errline);
throw $exception;
}
return false;
}
/**
* 记录异常
*/
private function log($exception) {
$logData = $this->prepareLogData($exception);
// 应用过滤器
$logData = $this->applyFilters($logData);
// 应用处理器
$logData = $this->applyProcessors($logData);
// 发送到所有处理器
$this->sendToHandlers($logData);
}
/**
* 准备日志数据
*/
private function prepareLogData($exception) {
$data = [
"timestamp" => date("c"),
"level" => $this->getExceptionLevel($exception),
"exception" => get_class($exception),
"message" => $exception->getMessage(),
"code" => $exception->getCode(),
"file" => $exception->getFile(),
"line" => $exception->getLine(),
"trace" => $exception->getTrace(),
"previous" => $exception->getPrevious() ? $this->prepareLogData($exception->getPrevious()) : null,
];
// 添加上下文信息
if (php_sapi_name() !== "cli") {
$data["context"] = [
"request" => [
"method" => $_SERVER["REQUEST_METHOD"] ?? null,
"uri" => $_SERVER["REQUEST_URI"] ?? null,
"query" => $_GET ?? [],
"body" => $_POST ?? [],
],
"user" => [
"ip" => $_SERVER["REMOTE_ADDR"] ?? null,
"agent" => $_SERVER["HTTP_USER_AGENT"] ?? null,
],
];
}
return $data;
}
/**
* 获取异常级别
*/
private function getExceptionLevel($exception) {
$exceptionClass = get_class($exception);
$levelMap = [
"Error" => "EMERGENCY",
"ParseError" => "EMERGENCY",
"TypeError" => "EMERGENCY",
"PDOException" => "CRITICAL",
"DatabaseException" => "CRITICAL",
"RuntimeException" => "ERROR",
"InvalidArgumentException" => "WARNING",
"LogicException" => "WARNING",
];
return $levelMap[$exceptionClass] ?? "ERROR";
}
/**
* 应用过滤器
*/
private function applyFilters($data) {
foreach ($this->config["filters"] as $filter) {
if (is_callable($filter)) {
$data = $filter($data);
}
}
return $data;
}
/**
* 应用处理器
*/
private function applyProcessors($data) {
foreach ($this->config["processors"] as $processor) {
if (is_callable($processor)) {
$data = $processor($data);
}
}
return $data;
}
/**
* 发送到处理器
*/
private function sendToHandlers($data) {
foreach ($this->config["handlers"] as $handler) {
if (is_callable($handler)) {
$handler($data);
}
}
}
/**
* 通知异常
*/
private function notify($exception) {
// 根据配置发送通知
// 这里可以实现邮件、Slack、短信等通知
}
}
// 使用示例
$logger = new CompleteExceptionLogger([
"handlers" => [
function($data) {
// 文件处理器
file_put_contents(
"logs/exceptions.json",
json_encode($data, JSON_PRETTY_PRINT) . ",\n",
FILE_APPEND
);
},
function($data) {
// 控制台处理器(开发环境)
if (getenv("APP_ENV") === "development") {
error_log(print_r($data, true));
}
},
],
"filters" => [
function($data) {
// 过滤敏感信息
if (isset($data["context"]["request"]["body"]["password"])) {
$data["context"]["request"]["body"]["password"] = "[FILTERED]";
}
return $data;
},
],
]);
$logger->init();';
echo "<pre style='background:#f8f9fa; padding:15px; border:1px solid #ddd; border-radius:3px; max-height:500px; overflow:auto;'>" .
htmlspecialchars($completeExample) . "</pre>";
echo "<div class='alert alert-info'>";
echo "这是一个完整的异常日志记录器实现,包含了最佳实践的所有要点。";
echo "</div>";
虽然PHP没有内置set_exception_logger()函数,但我们可以创建自定义函数或类来实现异常日志记录功能。以下是关键要点:
set_exception_handler()和自定义函数实现异常日志记录推荐使用成熟的日志库:在实际项目中,推荐使用Monolog等成熟的日志库,它们提供了丰富的功能和良好的性能。