PHPset_exception_logger()自定义函数

简介

set_exception_logger() 是一个自定义的PHP函数概念,用于设置专门记录异常到日志的自定义处理器。虽然PHP没有内置这个函数,但我们可以创建自己的实现,用于统一处理异常的日志记录。

注意:这不是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

示例实现

示例1:基本实现

实现一个简单的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 "这行代码不会执行";

示例2:完整的异常日志记录器类

创建一个完整的异常日志记录器类:

<?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("未捕获的测试异常");

示例3:支持多种存储后端的日志记录器

创建一个支持文件、数据库、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("测试多后端日志记录");

示例4:异常日志记录器的实际应用

演示在实际项目中如何使用异常日志记录器:

<?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>";

示例5:Monolog集成示例

演示如何使用流行的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("普通异常测试");

示例6:异常日志记录的最佳实践

总结异常日志记录的最佳实践和模式:

<?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()和自定义函数实现异常日志记录
  • 多种存储后端:支持文件、数据库、Elasticsearch等多种存储方式
  • 结构化日志:使用JSON等结构化格式记录日志,便于分析
  • 上下文信息:记录异常发生时的上下文信息,便于调试
  • 通知机制:支持邮件、Slack等多种通知方式
  • 性能考虑:异步记录日志,避免阻塞主流程
  • 安全考虑:过滤敏感信息,保护用户数据

推荐使用成熟的日志库:在实际项目中,推荐使用Monolog等成熟的日志库,它们提供了丰富的功能和良好的性能。