zoukankan      html  css  js  c++  java
  • Qt 自定义日志类

    一、前言

    C++ 中比较不错的日志工具有 log4cxxlog4qt 等,但是它们都不能和 qDebug(), qInfo() 等有机的结合在一起,所以在 Qt 中使用总觉得不够舒服,感谢 Qt 提供了 qInstallMessageHandler() 这个函数,使用这个函数可以安装自定义的日志输出处理函数,把日志输出到文件,控制台等,具体的使用可以查看 Qt 的帮助文档。

    本文主要是介绍使用 qInstallMessageHandler() 实现一个简单的日志工具,例如调用 qDebug() << "Hi",输出的内容会同时输出到日志文件和控制台,并且日志文件如果不是当天创建的,会使用它的创建日期备份起来,涉及到的文件有:

    • main.cpp: 使用示例
    • LogHandler.h: 自定义日志相关类的头文件
    • LogHandler.cpp: 自定义日志相关类的实现文件

    另外实现功能:

    • 单个日志文件例如大于 5M 后重新创建一个新的日志文件;

    • 删除超过 30 天的日志;

    • 使用锁确保多线程安全。

    后期考虑实现功能:

    • 日志的相关配置数据例如输出目录写到配置文件;
    • 日志可以选择存放在服务器。

    二、代码实现

    2.1 LogHandler.h

    #ifndef LOGHANDLER_H
    #define LOGHANDLER_H
    
    #include <iostream>
    #include <QDebug>
    #include <QDateTime>
    #include <QMutexLocker>
    #include <QDir>
    #include <QFile>
    #include <QFileInfo>
    #include <QTimer>
    #include <QTextStream>
    #include <QTextCodec>
    
    const int g_logLimitSize = 5;
    
    struct LogHandlerPrivate {
        LogHandlerPrivate();
        ~LogHandlerPrivate();
    
        // 打开日志文件 log.txt,如果日志文件不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
        void openAndBackupLogFile();
        void checkLogFiles(); // 检测当前日志文件大小
        void autoDeleteLog(); // 自动删除30天前的日志
    
        // 消息处理函数
        static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg);
    
        QDir   logDir;              // 日志文件夹
        QTimer renameLogFileTimer;  // 重命名日志文件使用的定时器
        QTimer flushLogFileTimer;   // 刷新输出到日志文件的定时器
        QDate  logFileCreatedDate;  // 日志文件创建的时间
    
        static QFile *logFile;      // 日志文件
        static QTextStream *logOut; // 输出日志的 QTextStream,使用静态对象就是为了减少函数调用的开销
        static QMutex logMutex;     // 同步使用的 mutex
    };
    
    class LogHandler {
    public:
        void installMessageHandler();   // 给Qt安装消息处理函数
        void uninstallMessageHandler(); // 取消安装消息处理函数并释放资源
    
        static LogHandler& Get() {
            static LogHandler m_logHandler;
            return m_logHandler;
        }
    
    private:
        LogHandler();
    
        LogHandlerPrivate *d;
    };
    
    #endif // LOGHANDLER_H
    

    2.2 LogHandler.cpp

    #include "LogHandler.h"
    
    /************************************************************************************************************
     *                                                                                                          *
     *                                               LogHandlerPrivate                                          *
     *                                                                                                          *
     ***********************************************************************************************************/
    // 初始化 static 变量
    QMutex LogHandlerPrivate::logMutex;
    QFile* LogHandlerPrivate::logFile = nullptr;
    QTextStream* LogHandlerPrivate::logOut = nullptr;
    
    LogHandlerPrivate::LogHandlerPrivate() {
        logDir.setPath("log"); // TODO: 日志文件夹的路径,为 exe 所在目录下的 log 文件夹,可从配置文件读取
        QString logPath = logDir.absoluteFilePath("today.log"); // 获取日志的路径
    
        // ========获取日志文件创建的时间========
        // QFileInfo::created(): On most Unix systems, this function returns the time of the last status change.
        // 所以不能运行时使用这个函数检查创建时间,因为会在运行时变化,于是在程序启动时保存下日志文件的最后修改时间,
        logFileCreatedDate = QFileInfo(logPath).lastModified().date(); // 若日志文件不存在,返回nullptr
    
        // 打开日志文件,如果不是当天创建的,备份已有日志文件
        openAndBackupLogFile();
    
        // 十分钟检查一次日志文件创建时间
        renameLogFileTimer.setInterval(1000 *  2); // TODO: 可从配置文件读取
        renameLogFileTimer.start();
        QObject::connect(&renameLogFileTimer, &QTimer::timeout, [this] {
            QMutexLocker locker(&LogHandlerPrivate::logMutex);
            openAndBackupLogFile(); // 打开日志文件
            checkLogFiles(); // 检测当前日志文件大小
            autoDeleteLog(); // 自动删除30天前的日志
        });
    
        // 定时刷新日志输出到文件,尽快的能在日志文件里看到最新的日志
        flushLogFileTimer.setInterval(1000); // TODO: 可从配置文件读取
        flushLogFileTimer.start();
        QObject::connect(&flushLogFileTimer, &QTimer::timeout, [] {
            // qDebug() << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); // 测试不停的写入内容到日志文件
            QMutexLocker locker(&LogHandlerPrivate::logMutex);
            if (nullptr != logOut) {
                logOut->flush();
            }
        });
    }
    
    LogHandlerPrivate::~LogHandlerPrivate() {
        if (nullptr != logFile) {
            logFile->flush();
            logFile->close();
            delete logOut;
            delete logFile;
    
            // 因为他们是 static 变量
            logOut  = nullptr;
            logFile = nullptr;
        }
    }
    
    // 打开日志文件 log.txt,如果不是当天创建的,则使用创建日期把其重命名为 yyyy-MM-dd.log,并重新创建一个 log.txt
    void LogHandlerPrivate::openAndBackupLogFile() {
        // 总体逻辑:
        // 1. 程序启动时 logFile 为 nullptr,初始化 logFile,有可能是同一天打开已经存在的 logFile,所以使用 Append 模式
        // 2. logFileCreatedDate is nullptr, 说明日志文件在程序开始时不存在,所以记录下创建时间
        // 3. 程序运行时检查如果 logFile 的创建日期和当前日期不相等,则使用它的创建日期重命名,然后再生成一个新的 log.txt 文件
        // 4. 检查日志文件超过 LOGLIMIT_NUM 个,删除最早的
        // 备注:log.txt 始终为当天的日志文件,当第二天,会执行第3步,将使用 log.txt 的创建日期重命名它
    
        // 如果日志所在目录不存在,则创建
        if (!logDir.exists()) {
            logDir.mkpath("."); // 可以递归的创建文件夹
        }
        QString logPath = logDir.absoluteFilePath("today.log"); // log.txt的路径
    
        // [[1]] 程序每次启动时 logFile 为 nullptr
        if (logFile == nullptr) {
            logFile = new QFile(logPath);
            logOut  = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) ?  new QTextStream(logFile) : nullptr;
            if (logOut != nullptr)
                logOut->setCodec("UTF-8");
    
            // [[2]] 如果文件是第一次创建,则创建日期是无效的,把其设置为当前日期
            if (logFileCreatedDate.isNull()) {
                logFileCreatedDate = QDate::currentDate();
            }
        }
    
        // [[3]] 程序运行时如果创建日期不是当前日期,则使用创建日期重命名,并生成一个新的 log.txt
        if (logFileCreatedDate != QDate::currentDate()) {
            logFile->flush();
            logFile->close();
            delete logOut;
            delete logFile;
    
            QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd.log"));;
            QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现
            QFile::remove(logPath); // 删除重新创建,改变创建时间
    
            // 重新创建 log.txt
            logFile = new QFile(logPath);
            logOut  = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ?  new QTextStream(logFile) : nullptr;
            logFileCreatedDate = QDate::currentDate();
            if (logOut != nullptr)
                logOut->setCodec("UTF-8");
        }
    }
    
    // 检测当前日志文件大小
    void LogHandlerPrivate::checkLogFiles() {
        // 如果 protocal.log 文件大小超过5M,重新创建一个日志文件,原文件存档为yyyy-MM-dd_hhmmss.log
        if (logFile->size() > 1024*g_logLimitSize) {
            logFile->flush();
            logFile->close();
            delete logOut;
            delete logFile;
    
            QString logPath = logDir.absoluteFilePath("today.log"); // 日志的路径
            QString newLogPath = logDir.absoluteFilePath(logFileCreatedDate.toString("yyyy-MM-dd.log"));
            QFile::copy(logPath, newLogPath); // Bug: 按理说 rename 会更合适,但是 rename 时最后一个文件总是显示不出来,需要 killall Finder 后才出现
            QFile::remove(logPath); // 删除重新创建,改变创建时间
    
            logFile = new QFile(logPath);
            logOut  = (logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) ?  new QTextStream(logFile) : NULL;
            logFileCreatedDate = QDate::currentDate();
            if (logOut != nullptr)
                logOut->setCodec("UTF-8");
        }
    }
    
    // 自动删除30天前的日志
    void LogHandlerPrivate::autoDeleteLog()
    {
        QDateTime now = QDateTime::currentDateTime();
    
        // 前30天
        QDateTime dateTime1 = now.addDays(-30);
        QDateTime dateTime2;
    
        QString logPath = logDir.absoluteFilePath("today.log"); // 日志的路径
        QDir dir(logPath);
        QFileInfoList fileList = dir.entryInfoList();
        foreach (QFileInfo f, fileList ) {
            // "."和".."跳过
            if (f.baseName() == "")
                continue;
    
            dateTime2 = QDateTime::fromString(f.baseName(), "yyyy-MM-dd");
            if (dateTime2 < dateTime1) { // 只要日志时间小于前30天的时间就删除
                dir.remove(f.absoluteFilePath());
            }
        }
    }
    
    // 消息处理函数
    void LogHandlerPrivate::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
        QMutexLocker locker(&LogHandlerPrivate::logMutex);
        QString level;
    
        switch (type) {
        case QtDebugMsg:
            level = "DEBUG";
            break;
        case QtInfoMsg:
            level = "INFO ";
            break;
        case QtWarningMsg:
            level = "WARN ";
            break;
        case QtCriticalMsg:
            level = "ERROR";
            break;
        case QtFatalMsg:
            level = "FATAL";
            break;
        default:
            break;
        }
    
        // 输出到标准输出: Windows 下 std::cout 使用 GB2312,而 msg 使用 UTF-8,但是程序的 Local 也还是使用 UTF-8
    #if defined(Q_OS_WIN)
        QByteArray localMsg = QTextCodec::codecForName("GB2312")->fromUnicode(msg); //msg.toLocal8Bit();
    #else
        QByteArray localMsg = msg.toLocal8Bit();
    #endif
    
        std::cout << std::string(localMsg) << std::endl;
    
        if (nullptr == LogHandlerPrivate::logOut) {
            return;
        }
    
        // 输出到日志文件, 格式: 时间 - [Level] (文件名:行数, 函数): 消息
        QString fileName = context.file;
        int index = fileName.lastIndexOf(QDir::separator());
        fileName = fileName.mid(index + 1);
    
        (*LogHandlerPrivate::logOut) << QString("%1 - [%2] (%3:%4, %5): %6
    ")
                                        .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")).arg(level)
                                        .arg(fileName).arg(context.line).arg(context.function).arg(msg);
    }
    
    /************************************************************************************************************
     *                                                                                                          *
     *                                               LogHandler                                                 *
     *                                                                                                          *
     ***********************************************************************************************************/
    LogHandler::LogHandler() : d(nullptr) {
    }
    
    // 给Qt安装消息处理函数
    void LogHandler::installMessageHandler() {
        QMutexLocker locker(&LogHandlerPrivate::logMutex); // 类似C++11的lock_guard,析构时自动解锁
    
        if (nullptr == d) {
            d = new LogHandlerPrivate();
            qInstallMessageHandler(LogHandlerPrivate::messageHandler); // 给 Qt 安装自定义消息处理函数
        }
    }
    
    // 取消安装消息处理函数并释放资源
    void LogHandler::uninstallMessageHandler() {
        QMutexLocker locker(&LogHandlerPrivate::logMutex);
    
        qInstallMessageHandler(nullptr);
        delete d;
        d = nullptr;
    }
    

    2.3 main.cpp

    #include "LogHandler.h"
    
    #include <QApplication>
    #include <QDebug>
    #include <QTime>
    #include <QPushButton>
    
    int main(int argc, char *argv[]) {
        QApplication app(argc, argv);
    
        // [[1]] 安装消息处理函数
        LogHandler::Get().installMessageHandler();
    
        // [[2]] 输出测试,查看是否写入到文件
        qDebug() << "Hello";
        qDebug() << "当前时间是: " << QTime::currentTime().toString("hh:mm:ss");
        qInfo() << QString("God bless you!");
    
        QPushButton *button = new QPushButton("退出");
        button->show();
        QObject::connect(button, &QPushButton::clicked, [&app] {
            qDebug() << "退出";
            app.quit();
        });
    
        // [[3]] 取消安装自定义消息处理,然后启用
        LogHandler::Get().uninstallMessageHandler();
        qDebug() << "........"; // 不写入日志
        LogHandler::Get().installMessageHandler();
    
        int ret = app.exec(); // 事件循环结束
    
        // [[4]] 程序结束时释放 LogHandler 的资源,例如刷新并关闭日志文件
        LogHandler::Get().uninstallMessageHandler();
    
        return ret;
    }
    

    如果想实现日志存放在服务器,可以参考:Qt 打印日志系统,实现打印日志按日期、大小保存,过期删除,窗口实时显示日志,网络传输日志远程调试


    2.4 运行效果

    控制台输出:

    Hello
    当前时间是:  "16:29:42"
    "God bless you!"
    ........
    退出
    

    日志文件(exe 所在目录的 log 目录下的 log.txt):

    16:29:42 - [Debug] (main.cpp:15, int main(int, char **)): Hello
    16:29:42 - [Debug] (main.cpp:16, int main(int, char **)): 当前时间是:  "16:29:42"
    16:29:42 - [Info ] (main.cpp:17, int main(int, char **)): "God bless you!"
    16:29:46 - [Debug] (main.cpp:22, auto main(int, char **)::(anonymous class)::operator()() const): 退出
    

    注意:

    Release 版本默认不包含文件名、函数名和行数信息,需要在 .pro 文件中加入一行代码,重新 make 运行后生效。

    DEFINES += QT_MESSAGELOGCONTEXT
    

    三、其他 C++ 日志框架

    C++ 中的日志框架有很多,其中比较著名的有:

    • log4cxx:Java 社区著名的 Log4j 的 C++ 移植版,用于为 C++ 程序提供日志功能,以便开发者对目标程序进行调试和审计。

    • log4cplus:一个简单易用的 C++ 日志记录 API,它提供了对日志管理和配置的线程安全、灵活和任意粒度控制(也基于 Log4j)。

    • Log4cpp:一个 C++ 类库,可以灵活地记录到文件、syslog、IDSA 和其他目的地(也基于 Log4j)。

    • google-glog:一个 C++ 语言的应用级日志记录框架,提供了 C++ 风格的流操作和各种辅助宏。

    • Pantheios:一个类型安全、高效、泛型和可扩展性的 C++ 日志 API 库(号称 C++ 领域速度最快的日志库)。

    • POCO:还提供了一个 好的日志支持文档。

    • ACE:ACE 也有日志支持。

    • Boost.Log:设计的非常模块化,并且可扩展。

    • Easylogging++:轻量级高性能 C++ 日志库(只有一个头文件)。

    • G3log:一个开源、支持跨平台的异步 C++ 日志框架,支持自定义日志格式。基于 g2log 构建,提升了性能,支持自定义格式。

    • Plog:可移植、简单和可扩展的 C++ 日志库。

    • spdlog:一个快速的 C++ 日志库,只包含头文件,兼容 C++11。

    • ……

    其中 log4cplus、glog 较为流行,一个是著名的 Log4j 的衍生品,另一个则是 Google 的“亲儿子”。

    包括 log4qt,也是 Log4j 的衍生品,可以参考:DevBean豆子大神的github


    参考:

    Qt 自定义日志工具

    简单易用的Qt日志模块

    C++ 日志框架


  • 相关阅读:
    IntelliJ IDEA 常用快捷键
    solr4.5分组查询、统计功能介绍
    用于Lucene的各中文分词比较
    Lucene打分规则与Similarity模块详解
    Lucene
    tar中的参数 cvf,xvf,cvzf,zxvf的区别
    tmux 入门踩坑记录
    第一个shell脚本
    make 和 make install 的区别
    交叉编译
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14716532.html
Copyright © 2011-2022 走看看