将 C++ 程序异常信息存入 MySQL 数据库的完整方案

在 C++ 程序中捕获异常并将异常信息存入 MySQL 数据库是一个常见的需求,特别是在需要长期记录和分析程序错误场景时。下面我将详细介绍几种实现方法,从基础到高级逐步讲解。

一、基础实现方案

1.1 使用MySQL C API直接存储异常

这种方法适合简单的 C++ 项目,直接使用 MySQL C API 连接数据库并插入异常信息。

#include <mysql/mysql.h>
#include <iostream>
#include <stdexcept>

// 全局数据库连接
MYSQL *conn = nullptr;

void init_db_connection() {
    conn = mysql_init(nullptr);
    if (!conn) {
        throw std::runtime_error("MySQL initialization failed");
    }
    
    if (!mysql_real_connect(conn, "localhost", "user", "password", 
                          "exception_db", 0, nullptr, 0)) {
        std::string err = "Connection error: ";
        err += mysql_error(conn);
        mysql_close(conn);
        throw std::runtime_error(err);
    }
}

void log_exception_to_db(const std::string& exception_type, 
                        const std::string& message,
                        const std::string& stack_trace,
                        const std::string& timestamp) {
    std::string query = "INSERT INTO exceptions (type, message, stack_trace, timestamp) VALUES ('" +
                       exception_type + "', '" + message + "', '" + stack_trace + "', '" + timestamp + "')";
    
    if (mysql_query(conn, query.c_str())) {
        std::cerr << "Error logging exception: " << mysql_error(conn) << std::endl;
    }
}

int main() {
    try {
        init_db_connection();
        // 程序主逻辑
        throw std::runtime_error("Sample exception for testing");
    } catch (const std::exception& e) {
        // 获取当前时间
        time_t now = time(nullptr);
        char timestamp[20];
        strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
        
        // 记录异常到数据库
        log_exception_to_db(typeid(e).name(), e.what(), "stack trace here", timestamp);
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    
    if (conn) mysql_close(conn);
    return 0;
}

注意事项

  • 需要先创建异常表:CREATE TABLE exceptions(id INT AUTO_INCREMENT PRIMARY KEY, type VARCHAR(255), message TEXT, stack_trace TEXT, timestamp DATETIME);

  • 此方法存在 SQL 注入风险,实际应用中应使用参数化查询

二、使用MySQL Connector/C++的高级方案

MySQL 官方提供的 Connector/C++ 库提供了更安全、面向对象的接口。

2.1 使用预编译语句防止SQL注入

#include <cppconn/driver.h>
#include <cppconn/exception.h>
#include <cppconn/prepared_statement.h>
#include <iostream>
#include <ctime>

std::unique_ptr<sql::Connection> get_db_connection() {
    try {
        sql::Driver* driver = get_driver_instance();
        std::unique_ptr<sql::Connection> conn(
            driver->connect("tcp://127.0.0.1:3306", "user", "password"));
        conn->setSchema("exception_db");
        return conn;
    } catch (sql::SQLException& e) {
        std::cerr << "DB Connection Error: " << e.what() << std::endl;
        throw;
    }
}

void log_exception(const std::exception& e, sql::Connection* conn) {
    try {
        std::unique_ptr<sql::PreparedStatement> pstmt(
            conn->prepareStatement(
                "INSERT INTO exceptions(type, message, timestamp) VALUES (?, ?, ?)"));
        
        // 获取当前时间戳
        std::time_t now = std::time(nullptr);
        std::tm* tm = std::localtime(&now);
        char timestamp[20];
        std::strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm);
        
        pstmt->setString(1, typeid(e).name());
        pstmt->setString(2, e.what());
        pstmt->setString(3, timestamp);
        
        pstmt->executeUpdate();
    } catch (sql::SQLException& db_err) {
        std::cerr << "Failed to log exception: " << db_err.what() << std::endl;
    }
}

int main() {
    try {
        auto conn = get_db_connection();
        // 程序主逻辑
        throw std::runtime_error("Test exception with Connector/C++");
    } catch (const std::exception& e) {
        try {
            auto conn = get_db_connection();
            log_exception(e, conn.get());
        } catch (...) {
            std::cerr << "Failed to log exception to DB" << std::endl;
        }
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    return 0;
}

优势

  • 使用预编译语句防止 SQL 注入

  • 面向对象接口更易用

  • 更好的异常处理机制

三、结合日志系统的综合方案

3.1 使用日志库+数据库的组合方案

对于生产环境,建议结合日志库 (如 spdlog) 和数据库存储:

#include <spdlog/spdlog.h>
#include <cppconn/driver.h>
#include <memory>

class DatabaseLogger {
public:
    DatabaseLogger() {
        try {
            driver_ = get_driver_instance();
            conn_ = std::unique_ptr<sql::Connection>(
                driver_->connect("tcp://127.0.0.1:3306", "user", "password"));
            conn_->setSchema("logging_db");
            
            // 创建表如果不存在
            std::unique_ptr<sql::Statement> stmt(conn_->createStatement());
            stmt->execute("CREATE TABLE IF NOT EXISTS exception_logs ("
                         "id INT AUTO_INCREMENT PRIMARY KEY,"
                         "timestamp DATETIME,"
                         "level VARCHAR(20),"
                         "type VARCHAR(255),"
                         "message TEXT,"
                         "context TEXT)");
        } catch (sql::SQLException& e) {
            spdlog::error("DatabaseLogger init failed: {}", e.what());
            throw;
        }
    }
    
    void log_exception(const std::exception& e, const std::string& context = "") {
        try {
            std::unique_ptr<sql::PreparedStatement> pstmt(
                conn_->prepareStatement(
                    "INSERT INTO exception_logs(timestamp, level, type, message, context) "
                    "VALUES (NOW(), 'error', ?, ?, ?)"));
            
            pstmt->setString(1, typeid(e).name());
            pstmt->setString(2, e.what());
            pstmt->setString(3, context);
            
            pstmt->executeUpdate();
            
            // 同时记录到文件日志
            spdlog::error("Exception occurred: {} - Context: {}", e.what(), context);
        } catch (sql::SQLException& db_err) {
            spdlog::error("Failed to log exception to DB: {}", db_err.what());
        }
    }
    
private:
    sql::Driver* driver_;
    std::unique_ptr<sql::Connection> conn_;
};

int main() {
    // 初始化日志系统
    spdlog::set_level(spdlog::level::debug);
    auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/exceptions.log");
    spdlog::set_default_logger(file_logger);
    
    try {
        DatabaseLogger db_logger;
        // 程序主逻辑
        throw std::runtime_error("Comprehensive exception test");
    } catch (const std::exception& e) {
        spdlog::error("Main exception caught: {}", e.what());
        
        // 获取堆栈信息 (简化版)
        std::string stack_trace = "Stack trace not available";
        #ifdef _WIN32
        // Windows下获取堆栈的方法
        #else
        // Linux下获取堆栈的方法
        #endif
        
        try {
            DatabaseLogger db_logger;
            db_logger.log_exception(e, stack_trace);
        } catch (...) {
            spdlog::critical("Failed to initialize database logger");
        }
    }
    
    return 0;
}

特点

  • 数据库和文件双日志记录

  • 包含更多上下文信息

  • 使用现代日志库 spdlog

  • 可扩展添加堆栈跟踪信息

四、异常处理最佳实践

4.1 异常分层处理

class DatabaseException : public std::runtime_error {
public:
    DatabaseException(const std::string& msg, int error_code)
        : std::runtime_error(msg), error_code_(error_code) {}
    
    int get_error_code() const { return error_code_; }
    
private:
    int error_code_;
};

void log_custom_exception(const DatabaseException& e) {
    try {
        sql::Driver* driver = get_driver_instance();
        std::unique_ptr<sql::Connection> conn(
            driver->connect("tcp://127.0.0.1:3306", "user", "password"));
        conn->setSchema("exception_db");
        
        std::unique_ptr<sql::PreparedStatement> pstmt(
            conn->prepareStatement(
                "INSERT INTO custom_exceptions(type, message, code, timestamp) "
                "VALUES (?, ?, ?, NOW())"));
        
        pstmt->setString(1, typeid(e).name());
        pstmt->setString(2, e.what());
        pstmt->setInt(3, e.get_error_code());
        
        pstmt->executeUpdate();
    } catch (sql::SQLException& db_err) {
        // 如果数据库记录失败,至少记录到本地日志
        spdlog::error("Failed to log custom exception: {}", db_err.what());
    }
}

4.2 使用RAII管理资源

class DBConnectionRAII {
public:
    DBConnectionRAII(const std::string& host, const std::string& user, 
                    const std::string& pass, const std::string& db)
        : conn_(nullptr) {
        conn_ = get_driver_instance()->connect(host, user, pass);
        conn_->setSchema(db);
    }
    
    ~DBConnectionRAII() {
        if (conn_) {
            try {
                conn_->close();
            } catch (sql::SQLException& e) {
                spdlog::error("Failed to close DB connection: {}", e.what());
            }
            delete conn_;
        }
    }
    
    sql::Connection* get() { return conn_; }
    
private:
    sql::Connection* conn_;
};

五、性能优化与安全考虑

5.1 批量插入与连接池

对于高频异常场景,考虑使用连接池和批量插入:

class ExceptionLogger {
public:
    ExceptionLogger(size_t pool_size = 5) {
        for (size_t i = 0; i < pool_size; ++i) {
            auto conn = std::make_shared<sql::Connection>(
                get_driver_instance()->connect("tcp://127.0.0.1:3306", "user", "password"));
            conn->setSchema("exception_db");
            conn_pool_.push(conn);
        }
    }
    
    void batch_log(const std::vector<std::exception_ptr>& exceptions) {
        auto conn = get_connection();
        try {
            conn->setAutoCommit(false);
            
            std::unique_ptr<sql::PreparedStatement> pstmt(
                conn->prepareStatement(
                    "INSERT INTO batched_exceptions(type, message, timestamp) "
                    "VALUES (?, ?, NOW())"));
            
            for (auto& ex_ptr : exceptions) {
                try {
                    std::rethrow_exception(ex_ptr);
                } catch (const std::exception& e) {
                    pstmt->setString(1, typeid(e).name());
                    pstmt->setString(2, e.what());
                    pstmt->addBatch();
                }
            }
            
            pstmt->executeBatch();
            conn->commit();
        } catch (...) {
            conn->rollback();
            throw;
        } finally {
            release_connection(conn);
        }
    }
    
private:
    std::shared_ptr<sql::Connection> get_connection() {
        std::lock_guard<std::mutex> lock(pool_mutex_);
        if (conn_pool_.empty()) {
            throw std::runtime_error("Connection pool exhausted");
        }
        auto conn = conn_pool_.front();
        conn_pool_.pop();
        return conn;
    }
    
    void release_connection(std::shared_ptr<sql::Connection> conn) {
        std::lock_guard<std::mutex> lock(pool_mutex_);
        conn_pool_.push(conn);
    }
    
    std::queue<std::shared_ptr<sql::Connection>> conn_pool_;
    std::mutex pool_mutex_;
};

5.2 安全注意事项

  1. 敏感信息处理:异常信息可能包含敏感数据,应进行脱敏处理

  2. 防 SQL 注入:始终使用参数化查询

  3. 错误处理:数据库记录失败时应有备用方案(如本地日志)

  4. 连接安全:使用 SSL 加密数据库连接

  5. 权限控制:数据库用户应只有必要的最小权限

六、完整示例项目结构

exception_logging_system/
├── include/
│   ├── database_logger.h      # 数据库日志接口
│   ├── exception_handler.h    # 异常处理核心逻辑
│   └── utilities.h            # 辅助函数(如时间处理)
├── src/
│   ├── database_logger.cpp   # 数据库日志实现
│   ├── exception_handler.cpp # 异常处理实现
│   ├── main.cpp              # 应用入口
│   └── CMakeLists.txt        # 构建配置
├── third_party/              # 第三方库(如mysql-connector)
└── tests/                    # 单元测试

构建建议

  • 使用 CMake 管理项目

  • 将 MySQL Connector/C++ 作为依赖项

  • 添加 spdlog 或其他日志库

七、扩展功能思路

  1. 异常分析面板:基于数据库中的异常数据开发 Web 分析界面

  2. 自动警报系统:对特定类型或频率的异常触发警报

  3. 机器学习分类:对异常自动分类和优先级排序

  4. 分布式收集:在微服务架构中集中收集所有服务的异常

  5. 性能监控集成:将异常与性能指标关联分析

通过以上方案,您可以构建一个健壮的 C++ 异常日志系统,将异常信息可靠地存入 MySQL 数据库,为后续的分析和调试提供完整的历史记录。根据项目规模和需求,可以选择从简单到复杂的不同实现方案。


将 C++ 程序异常信息存入 MySQL 数据库的完整方案
https://uniomo.com/archives/jiang-c-cheng-xu-yi-chang-xin-xi-cun-ru-mysqlshu-ju-ku-de-wan-zheng-fang-an
作者
雨落秋垣
发布于
2024年06月22日
许可协议